lisa-bose-soundtouch
Version:
Bose SoundTouch node js API
389 lines (319 loc) • 11.7 kB
JavaScript
const http = require('http');
const parser = require('./utils/xmltojson');
const request = require('request');
const WebSocketClient = require('websocket').client;
const KEYS = require('./utils/types').Keys;
const SOURCES = require('./utils/types').Source;
/**
*
* Device should at least contain
* {
* name: 'Kitchen',
* ip: '',
* mac_address: ''
* }
*
* @param device
* @constructor
*/
const SoundTouchAPI = function (device) {
device.url = 'http://' + device.ip + ':' + device.port;
device.ws_url = 'ws://' + device.ip + ':' + '8080';
this.device = device;
this.name = device.name;
this.socket = {
source: undefined
};
};
SoundTouchAPI.prototype.getDevice = function () {
return this.device;
};
SoundTouchAPI.prototype.getMetaData = function () {
return this.device;
};
SoundTouchAPI.prototype.getNowPlaying = function (handler) {
this._getForDevice('now_playing', handler);
};
SoundTouchAPI.prototype.getTrackInfo = function (handler) {
this._getForDevice('trackInfo', handler);
};
SoundTouchAPI.prototype.getPresets = function (handler) {
this._getForDevice('presets', handler);
};
SoundTouchAPI.prototype.getSources = function (handler) {
this._getForDevice('sources', handler);
};
SoundTouchAPI.prototype.getInfo = function (handler) {
this._getForDevice('info', handler);
};
SoundTouchAPI.prototype.isAlive = function (handler) {
this.getNowPlaying(function (json) {
if (json === undefined) {
handler(false);
return;
}
let isAlive = json.nowPlaying.attributes.source !== SOURCES.STANDBY;
if (isAlive) {
isAlive = json.nowPlaying.playStatus.text === 'PLAY_STATE';
}
handler(isAlive);
});
};
SoundTouchAPI.prototype.isPoweredOn = function (handler) {
this.getNowPlaying(function (json) {
if (json === undefined) {
handler(false);
return;
}
const isAlive = json.nowPlaying.attributes.source !== SOURCES.STANDBY;
handler(isAlive);
});
};
SoundTouchAPI.prototype.getVolume = function (handler) {
this._getForDevice('volume', handler);
};
SoundTouchAPI.prototype.setVolume = function (volume, handler) {
const data = '<volume>' + volume + '</volume>';
this._setForDevice('volume', data, handler);
};
SoundTouchAPI.prototype.select = function (source, type, sourceAccount, location, handler) {
if (source === undefined) {
throw new Error('Source is not optional, provide a source from the SOURCES list.');
}
const data = '<ContentItem source="' + source + '" type="' + type + '" sourceAccount="' + sourceAccount + '" location="' + location + '">' +
'<itemName>' + 'Select using API' + '</itemName>' +
'</ContentItem>';
this._setForDevice('select', data, handler);
};
SoundTouchAPI.prototype.setName = function (name, handler) {
const data = '<name>' + name + '</name>';
this._setForDevice('name', data, handler);
};
SoundTouchAPI.prototype.play = function (handler) {
this.pressKey(KEYS.PLAY, handler);
};
SoundTouchAPI.prototype.stop = function (handler) {
this.pressKey(KEYS.STOP, handler);
};
SoundTouchAPI.prototype.next = function (handler) {
this.pressKey(KEYS.NEXT_TRACK, handler);
};
SoundTouchAPI.prototype.previous = function (handler) {
this.pressKey(KEYS.PREV_TRACK, handler);
};
SoundTouchAPI.prototype.pause = function (handler) {
this.pressKey(KEYS.PAUSE, handler);
};
SoundTouchAPI.prototype.playPause = function (handler) {
this.pressKey(KEYS.PLAY_PAUSE, handler);
};
SoundTouchAPI.prototype.power = function (handler) {
this.pressKey(KEYS.POWER, handler);
};
SoundTouchAPI.prototype.powerOn = function (handler) {
const api = this;
api.isPoweredOn(function (isPoweredOn) {
if (!isPoweredOn) {
api.pressKey(KEYS.POWER, function () {
handler(true);
});
} else {
handler(false);
}
});
};
SoundTouchAPI.prototype.powerOnWithVolume = function (volume, handler) {
const api = this;
api.isPoweredOn(function (isPoweredOn) {
if (!isPoweredOn) {
api.pressKey(KEYS.POWER, function () {
api.setVolume(volume, function () {
handler(true);
});
});
} else {
handler(false);
}
});
};
SoundTouchAPI.prototype.powerOff = function (handler) {
const api = this;
api.isPoweredOn(function (isPoweredOn) {
if (isPoweredOn) {
api.pressKey(KEYS.POWER, function () {
handler(true);
});
} else {
handler(false);
}
});
};
SoundTouchAPI.prototype.pressKey = function (key, handler) {
const press = '<key state="press" sender="Gabbo">' + key + '</key>';
const release = '<key state="release" sender="Gabbo">' + key + '</key>';
const api = this;
api._setForDevice('key', press, function (json) {
api._setForDevice('key', release, handler);
});
};
SoundTouchAPI.prototype.getZone = function (handler) {
this._getForDevice('getZone', handler);
};
SoundTouchAPI.prototype.setZone = function (members, handler) {
this._zones('setZone', members, handler);
};
SoundTouchAPI.prototype.addZoneSlave = function (members, handler) {
this._zones('addZoneSlave', members, handler);
};
SoundTouchAPI.prototype.removeZoneSlave = function (members, handler) {
this._zones('removeZoneSlave', members, handler);
};
SoundTouchAPI.prototype._zones = function (action, members, handler) {
const item = {};
// the below line looked like it might have been a copy/paste error from discovery.js? master is undefined here.
// item.master = master;
let data = '<zone master="' + this.getDevice().txtRecord.MAC + '" senderIPAddress="127.0.0.1">';
for (const i in members) {
const member = members[i];
if (i === 0) {
item.slaves = [];
item.slaves.push(member);
data += '<member>' + member + '</member>';
} else {
item.slaves.push(member);
data += '<member>' + member + '</member>';
}
}
data += '</zone>';
this._setForDevice(action, data, function (json) {
handler(json, item);
});
};
/*
****** WEB SOCKETS ***********
*/
SoundTouchAPI.prototype.socketStart = function (successCallback, errorCallback) {
if (this.client !== undefined) {
return;
}
this.client = new WebSocketClient();
const api = this;
this.client.on('connect', function (connection) {
if (successCallback !== undefined) successCallback();
connection.on('error', function (error) {
if (errorCallback !== undefined) errorCallback(error.toString());
});
connection.on('close', function () {
api.client = undefined;
});
connection.on('message', function (message) {
if (message.type === 'utf8') {
const json = parser.convert(message.utf8Data);
if (json.updates) {
api.socketUpdate(json.updates);
}
}
});
});
this.client.on('connectFailed', function (error) {
if (errorCallback !== undefined) errorCallback(error.toString());
});
this.client.connect(this.getMetaData().ws_url, 'gabbo');
};
SoundTouchAPI.prototype.socketUpdate = function (json) {
if (json.nowPlayingUpdated !== undefined) {
if (this.socket.nowPlayingUpdatedListener !== undefined) {
this.socket.nowPlayingUpdatedListener(json.nowPlayingUpdated);
}
//special listener: Powered On // Powered Off
const source = json.nowPlayingUpdated.nowPlaying.attributes.source;
if (this.socket.source !== source) {
this.socket.source = source;
if (this.socket.poweredListener !== undefined) {
this.socket.poweredListener(source !== SOURCES.STANDBY, json.nowPlayingUpdated.nowPlaying);
}
}
//special listener: Playing // Not Playing
const playStatus = json.nowPlayingUpdated.nowPlaying.playStatus ? json.nowPlayingUpdated.nowPlaying.playStatus.text : null;
if (this.socket.playStatus !== playStatus) {
this.socket.playStatus = playStatus;
if (this.socket.isPlayingListener !== undefined) {
this.socket.isPlayingListener(playStatus === 'PLAY_STATE', json.nowPlayingUpdated.nowPlaying);
}
}
} else if (json.volumeUpdated !== undefined) {
this.socket.volume = json.volumeUpdated.volume.actualvolume.text;
if (this.socket.volumeUpdatedListener !== undefined) {
this.socket.volumeUpdatedListener(json.volumeUpdated.volume.actualvolume.text, json.volumeUpdated);
}
} else if (json.connectionStateUpdated !== undefined) {
if (this.socket.connectionStateUpdatedListener !== undefined) {
this.socket.connectionStateUpdatedListener(json.connectionStateUpdated);
}
} else if (json.nowSelectionUpdated !== undefined) {
if (this.socket.nowSelectionUpdatedListener !== undefined) {
this.socket.nowSelectionUpdatedListener(json.nowSelectionUpdated);
}
} else if (json.recentsUpdated !== undefined) {
if (this.socket.recentsUpdatedListener !== undefined) {
this.socket.recentsUpdatedListener(json.recentsUpdated);
}
} else if (json.zoneUpdated !== undefined) {
if (this.socket.zoneUpdatedListener !== undefined) {
this.socket.zoneUpdatedListener(json.zoneUpdated);
}
} else {
console.log('Other update', json);
}
};
SoundTouchAPI.prototype.setNowPlayingUpdatedListener = function (handler) {
this.socket.nowPlayingUpdatedListener = handler;
};
SoundTouchAPI.prototype.setPoweredListener = function (handler) {
this.socket.poweredListener = handler;
};
SoundTouchAPI.prototype.setIsPlayingListener = function (handler) {
this.socket.isPlayingListener = handler;
};
SoundTouchAPI.prototype.setZoneUpdatedListener = function (handler) {
this.socket.zoneUpdatedListener = handler;
};
SoundTouchAPI.prototype.setVolumeUpdatedListener = function (handler) {
this.socket.volumeUpdatedListener = handler;
};
SoundTouchAPI.prototype.setConnectionStateUpdatedListener = function (handler) {
this.socket.connectionStateUpdatedListener = handler;
};
SoundTouchAPI.prototype.setNowSelectionUpdatedListener = function (handler) {
this.socket.nowSelectionUpdatedListener = handler;
};
SoundTouchAPI.prototype.setRecentsUpdatedListener = function (handler) {
this.socket.recentsUpdatedListener = handler;
};
/*
****** UTILITY METHODS ***********
*/
SoundTouchAPI.prototype._getForDevice = function (action, callback) {
const device = this.getMetaData();
http.get(device.url + '/' + action, function (response) {
parser.convertResponse(response, function (json) {
callback(json);
});
})
.on('error', function (e) {
console.error('Got error: ' + e.message);
throw new Error(e.message);
});
};
SoundTouchAPI.prototype._setForDevice = function (action, data, handler) {
const device = this.getDevice();
const options = {
url: device.url + '/' + action,
form: data
};
request.post(options, function (err, httpResponse, body) {
const json = parser.convert(body);
handler(json);
});
};
module.exports = SoundTouchAPI;