Bose T20 Serial Control

So here is my Bose T20 plugin, the volume feedback code is probably a little clunky but it does work. Unfortunately the protocol used by Bose seems to be a little bit out of the ordinary.

At some point in the future I plan on adding additional functionality as per the API, but i haven’t got round to that yet, if anyone needs a copy of the API document I can upload it somewhere for everyone.

In terms of functionality this version so far covers:

  • Volume Up
  • Volume Down
  • Volume Feedback - so you can have a slider in your remote
  • Mute
  • Power Toggle
plugin.Name = "BOSE_T20";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 1200000;
plugin.DefaultSettings = {
    "Host"  : "192.168.10.90",
    "Port"  : "4999",
};


var socket = new TCPClient();

var mediaCommandMappings = {
    "VolumeUp"      : "\x0b\x00\x01\x04\x03\x00\x2c\x01\x00\x00\x20\x0d",
    "VolumeDown"       : "\x0b\x00\x01\x04\x02\x00\x2c\x01\x00\x00\x21\x0d",
    "Mute"       : "\x0b\x00\x01\x04\x01\x00\x2c\x01\x00\x00\x22\x0d",
    "PowerToggle"   : "\x0b\x00\x01\x04\x4C\x00\x2c\x01\x00\x00\x6F\x0d",

};

var inputSourceMappings = {

};


function onChangeRequest(device, attribute, value) {
    console.log(plugin.Name + " onChangeRequest called...");
    var cmd;
    switch(attribute) {
        case "MediaCommand":
        case "InputSource":
        case "Volume":

            if(attribute == "MediaCommand") {
                cmd = mediaCommandMappings[value];
                console.log(plugin.Name + cmd);
            } else if(attribute == "InputSource") {
                cmd = inputSourceMappings[value];
            } else if(attribute == "Volume") {
            	//hexvol=20
        		cs = 0x07 ^ 0x00 ^ 0x01 ^ 0x15 ^ 0x01 ^ value
        		console.log("CS" + cs)
				cmd = [0x07, 0x00, 0x01, 0x15, 0x01, value, cs];
				console.log("cmd:" + cmd);
				//socket.send(cmd);
				//break;
			}
			
            if(cmd) {
                console.log(plugin.Name + " sending command: " + cmd);
                socket.send(cmd);
				console.log(plugin.Name + " Received: " + socket.receive({encoding: null}))
				GetVolume();
            } else {
                console.log(plugin.Name + " ERROR: unsupported command: " + value);
            }
            break;

        default:
            console.log("ERROR: unsupported attribute type: " + attribute);
            break;
    }
}

function onConnect() {
	console.log(plugin.Name + " onConnect called...");

    var host = plugin.Settings["Host"];
    var port = plugin.Settings["Port"];

    console.log(plugin.Name + "  Host: " + host);
    console.log(plugin.Name + "  Port: " + port);    
    console.log(plugin.Name + "  connecting...");
    socket.connect(host, port);
    console.log(plugin.Name + "  connected");
}

function onDisconnect() {
    console.log("onDisconnect called...");
    socket.close();
    console.log("  disconnected");
}

function GetVolume(){
	var loop=0;
    while(loop<5){
    socket.send("\x07\x00\x01\x15\x00\x1e\x0d");
    sleep(150);
    var data = socket.receive({encoding: null});
    console.log(plugin.Name + " volume data received: " + data);
    var hexdata="";
    var tempbit="";
    var i=0;
    while (i <data.length) {    // loop till all decimals are converted to hex
    tempbit=data[i].toString(16); // Get one Hex
    if(tempbit.length<2){tempbit="0"+tempbit;}  // if it is not 2 letters/number add a 0 in front for easier reading
  	hexdata=hexdata+tempbit + " ";  // add that hex value to hexdata and seperate them with a space
  	i=i+1;
	}
	console.log(plugin.Name + " volume hexdata: " + hexdata)
    //We look to see if the power is on or not we need to remove the sysReady String
    data = data.toString();
    var bytes = data.split(",");
    var device = plugin.Devices["BOSE_T20"];
    //first byte should be 5 if volume command
    if(bytes[2]==1&&bytes[3]==21){
    	console.log(plugin.Name + " split string: " + bytes[4]);
    	device.Volume = bytes[4];
    	loop=10;
    }else{
    	console.log(plugin.name + " Not volume Feedback!");
    	loop=loop + 1;
    }
    }
	
}

function onPoll() {
    console.log(plugin.Name + " onPoll called...");
    //We need to check the status of the system to confirm if power is on.
    socket.send("\x07\x00\x01\x1d\x00\x00\x1b\x0d");
    var data = socket.receive({encoding: null});
    console.log(plugin.Name + " status data received: " + data);
    //We look to see if the power is on or not we need to remove the sysReady String
    var hexdata="";
    var tempbit="";
    var i=0;
    while (i <data.length) {    // loop till all decimals are converted to hex
    	tempbit=data[i].toString(16); // Get one Hex
    	if(tempbit.length<2){tempbit="0"+tempbit;}  // if it is not 2 letters/number add a 0 in front for easier reading
  		hexdata=hexdata+tempbit + " ";  // add that hex value to hexdata and seperate them with a space
  		i=i+1;
	}
    console.log(plugin.Name + " status hexdata: " + hexdata)
    data = data.toString();
    var bytes = data.split(",");
    console.log(plugin.Name + " split string 1: " + bytes[0]);
    console.log(plugin.Name + " split string 5: " + bytes[4]);
    if(bytes[2]==1&&bytes[3]==29&&bytes[4]==0){
    	console.log(plugin.Name + ":Power Off");
    	throw "Bose-Power Off"
    }else if(bytes[2]==1&&bytes[3]==29&&bytes[4]==1){
    	GetVolume();
    }
}

function onSynchronizeDevices() {
    console.log("onSynchronizeDevices called...");

    var device = new Device();
    device.Id = plugin.Name;
    device.DisplayName = "Bose T20";
    device.Icon = "TV";
    device.Capabilities = [ "MediaControl", "MediaInputSource", "AudioVolume" ];
    device.Attributes = [  ];
    plugin.Devices[plugin.Name] = device;

    console.log("  done syncing");
}
1 Like