Skip to content

Commit

Permalink
MQTT Topic changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Floris Jan Galesloot committed Sep 30, 2020
1 parent 05c4415 commit f384572
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 77 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ You can also run this extension as a docker container. Example command:

The extension subscribes to all zone updates and pushes all info it gets from the Zone object found on https://roonlabs.github.io/node-roon-api-transport/Zone.html defined by the node-roon-api-transport service. It prepends the data with "roon/[zone-name]/...".

The MQTT topci for the 1 Line Now Playing information for a zone called Zone1 is: `roon/Zone1/now_playing/one_line/line1`.
The MQTT topci for the 1 Line Now Playing information for a zone called Zone1 is: `roon/zone1/now_playing/one_line/line1`.

As the characters +, /, # and space are illegal MQTT topic characters, those will be replaced by -. So if the name of a zone is `Kitchen / Living Room` you should use the topic `Kitchen---Living-Room` when subscribing and/or publishing. The same logic applies to the `[output-name]` descibed below.

The extension will ignore `+ 1` and similar when zones are grouped. So a grouped zone named `Kitchen + 2` will need to be addressed as `Kitchen` when you want to publish or subscribe to topics.

You can also see all published MQTT methods by uncommenting line 19 in app.js. Be aware that this will create a much larger log file.



### Control

To control a zone or an output, push a MQTT message to a zone/output like the following examples:
Expand All @@ -45,6 +51,7 @@ Available commands to use as message are defined by the RoonApiTransport: `play
To set the volume for a zone use the syntax:

Set volume to 65 for output: publish to `roon/[zone-name]/outputs/[output-name]/volume/set` with message `65`
Mute or Unmute are also supported. Simpy publish the message `mute` or `unmute` to the same topic. When setting the volume the unmute command will also be sent to the output.

### Browsing (beta only)

Expand Down
180 changes: 104 additions & 76 deletions roon-mqtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ var RoonApi = require("node-roon-api"),

function mqtt_publish_JSON( mqttbase, mqtt_client, jsondata ) {
if ( mqtt_client && mqtt_client.connected ) {
// mqtt_client.publish('roon/online','true');
for ( var attribute in jsondata ) {
var attributeTopic = toMqttTopic(attribute);
if ( typeof jsondata[attribute] === 'object' ) {
mqtt_publish_JSON( mqttbase+'/'+attribute, mqtt_client, jsondata[attribute] );
} else if ( typeof mqtt_data[mqttbase+'/'+attribute] === 'undefined' || mqtt_data[mqttbase+'/'+attribute] != jsondata[attribute].toString()) {
mqtt_publish_JSON( mqttbase+'/'+attributeTopic, mqtt_client, jsondata[attribute] );
} else if ( typeof mqtt_data[mqttbase+'/'+attributeTopic] === 'undefined' || mqtt_data[mqttbase+'/'+attributeTopic] != jsondata[attribute].toString()) {
if ( trace ) { console.log('*** sending MQTT: '+mqttbase+'/'+attribute+'='+jsondata[attribute]); }
mqtt_data[mqttbase+'/'+attribute] = jsondata[attribute].toString();
mqtt_client.publish(mqttbase+'/'+attribute,jsondata[attribute].toString());
mqtt_data[mqttbase+'/'+attributeTopic] = jsondata[attribute].toString();
mqtt_client.publish(mqttbase+'/'+attributeTopic,jsondata[attribute].toString());

} else {
if ( trace ) { console.log( '*** mqtt_publish_JSON nothing to publish to %s', mqttbase ); }
Expand Down Expand Up @@ -85,78 +85,86 @@ function mqtt_get_client() {
let topic_split = topic.split("/");
if ( typeof roon_core !== 'undefined' && topic_split[0] === mysettings.mqttroot ) {
if ( debug ) { console.log('*** we know of zones: %s', Object.keys(roon_zones) );}
if ( typeof roon_zones[topic_split[1]] === "undefined" ) {
let roon_zone = roonzone_find_by_mqtttopic(topic_split[1]);
if ( roon_zone == null ) {
console.log('*** zone %s not found!', topic_split[1] );
} else if ( topic_split[2] === 'command' && topic_split.length == 3) {
// Control entire zone
if ( debug ) { console.log('*** sending command %s to zone with id=%s', message, roon_zones[topic_split[1]]["zone_id"] );}
roon_core.services.RoonApiTransport.control(roon_zones[topic_split[1]]["zone_id"], message.toString());
} else if ( topic_split[2] === 'command' && topic_split.length == 4) {
// Control single output in zone
let output = roonzone_find_output_by_name(topic_split[1],topic_split[3]);
if ( output == null ) {
console.log('*** output %s not found in zone %s!', topic_split[3], topic_split[1] );
} else {
if ( debug ) { console.log('*** sending command %s to output with id=%s in zone=%s', message, output["output_id"], topic_split[1] );}
roon_core.services.RoonApiTransport.control(output["output_id"], message.toString());
}
} else if (topic_split[2] === 'outputs' && topic_split[4] === 'volume' && topic_split[5] === 'set') {
if ( debug ) { console.log('*** find output id for zone=%s, output=%s', topic_split[1], topic_split[3]) ;}
let output = roonzone_find_output_by_name(topic_split[1],topic_split[3]);
if ( output == null ) {
console.log('*** output %s not found in zone %s!', topic_split[3], topic_split[1] );
} else if ( message === 'mute' ) {
roon_core.services.RoonApiTransport.mute(output["output_id"], "mute" );
} else if ( message === 'unmute' ) {
roon_core.services.RoonApiTransport.mute(output["output_id"], "unmute" );
} else {
roon_core.services.RoonApiTransport.change_volume(output["output_id"], "absolute", parseInt(message), function() {
roon_core.services.RoonApiTransport.mute(output["output_id"], "unmute" );
});
}
} else if (topic_split[2] === 'outputs' && topic_split.length == 4 ) {
if ( debug ) { console.log('*** %s output %s to zone=%s', topic_split[3], message, topic_split[1]) ;}
let output = roonoutput_find_by_name(message);
if ( output != null ) {
let curoutputs = [];
for ( var index in roon_zones[topic_split[1]]["outputs"] ) {
curoutputs.push(roon_zones[topic_split[1]]["outputs"][index]["output_id"]);
}
if ( trace ) { console.log('*** curoutputs=%s output["output_id"]=%s', curoutputs, output["output_id"] ) ;}
if ( topic_split[3] === 'add' ) {
if ( !curoutputs.includes(output["output_id"]) ) {
if ( roon_zones[topic_split[1]].outputs[Object.keys(roon_zones[topic_split[1]].outputs)[0]].can_group_with_output_ids.includes(output["output_id"]) ) {
curoutputs.push(output["output_id"]);
roon_core.services.RoonApiTransport.group_outputs(curoutputs);
} else {
let zonename = roon_zone["display_name"];
if ( topic_split[2] === 'command' && topic_split.length == 3) {
// Control entire zone
if ( debug ) { console.log('*** sending command %s to zone with id=%s', message, roon_zone["zone_id"] );}
roon_core.services.RoonApiTransport.control(roon_zone["zone_id"], message.toString());
} else if ( topic_split[2] === 'command' && topic_split.length == 4) {
// Control single output in zone
let output = roonzone_find_output_by_name(zonename,topic_split[3]);
if ( output == null ) {
console.log('*** output %s not found in zone %s!', topic_split[3], zonename );
} else {
if ( debug ) { console.log('*** sending command %s to output with id=%s in zone=%s', message, output["output_id"], zonename );}
roon_core.services.RoonApiTransport.control(output["output_id"], message.toString());
}
} else if (topic_split[2] === 'outputs' && topic_split[4] === 'volume' && topic_split[5] === 'set') {
if ( debug ) { console.log('*** find output id for zone=%s, output=%s', zonename, topic_split[3]) ;}
let output = roonzone_find_output_by_name(zonename,topic_split[3]);
if ( output == null ) {
console.log('*** output %s not found in zone %s!', topic_split[3], zonename );
} else if ( typeof message === 'undefined' || message == '') {
console.log('*** no message for volume set command!');
} else if ( message.toString().toLowerCase() == 'mute' ) {
roon_core.services.RoonApiTransport.mute(output["output_id"], "mute" );
} else if ( message.toString().toLowerCase() == 'unmute' ) {
roon_core.services.RoonApiTransport.mute(output["output_id"], "unmute" );
} else if ( !isNaN(message) ) {
roon_core.services.RoonApiTransport.change_volume(output["output_id"], "absolute", parseFloat(message.toString()), function() {
roon_core.services.RoonApiTransport.mute(output["output_id"], "unmute" );
});
} else if ( debug ) {
console.log('*** invalid message for volume/set topic message=%s',message )
}
} else if (topic_split[2] === 'outputs' && topic_split.length == 4 ) {
if ( debug ) { console.log('*** %s output %s to zone=%s', topic_split[3], message, zonename) ;}
let output = roonoutput_find_by_name(message);
if ( output != null ) {
let curoutputs = [];
for ( var index in roon_zone["outputs"] ) {
curoutputs.push(roon_zone["outputs"][index]["output_id"]);
}
if ( trace ) { console.log('*** curoutputs=%s output["output_id"]=%s', curoutputs, output["output_id"] ) ;}
if ( topic_split[3] === 'add' ) {
if ( !curoutputs.includes(output["output_id"]) ) {
if ( roon_zone.outputs[Object.keys(roon_zone.outputs)[0]].can_group_with_output_ids.includes(output["output_id"]) ) {
curoutputs.push(output["output_id"]);
roon_core.services.RoonApiTransport.group_outputs(curoutputs);
} else if ( debug ) {
console.log('*** output %s cannot begrouped in zone %s', message, zonename );
}
} else if ( debug ) {
console.log('*** output %s cannot begrouped in zone %s', message, topic_split[1] );
console.log('*** output %s already grouped in zone %s', message, zonename );
}
} else if ( topic_split[3] === 'remove' ) {
if ( curoutputs.includes(output["output_id"]) ) {
roon_core.services.RoonApiTransport.ungroup_outputs([output["output_id"]]);
} else if ( debug ) {
console.log('*** output %s not found in zone %s', message, zonename );
}
} else if ( debug ) {
console.log('*** output %s already grouped in zone %s', message, topic_split[1] );
}
} else if ( topic_split[3] === 'remove' ) {
if ( curoutputs.includes(output["output_id"]) ) {
roon_core.services.RoonApiTransport.ungroup_outputs([output["output_id"]]);
} else if ( debug ) {
console.log('*** output %s not found in zone %s', message, topic_split[1] );
}
} else if ( debug ) {
console.log('*** output %s not found', message) ;
}
} else if ( debug ) {
console.log('*** output %s not found', message) ;
}
} else if ( topic_split[2] === 'browse' && topic_split.length == 4) {
let zoneId = roon_zones[topic_split[1]]["zone_id"];
let hierarchy = topic_split[3].toString().toLowerCase();
let action = {};
try {
action = JSON.parse(message.toString().toLowerCase());
} catch (e) {
console.log('*** no valid JSON in message. Assume only title is passed. message: %s', message.toString() );
action.title = message.toString().toLowerCase();
} else if ( topic_split[2] === 'browse' && topic_split.length == 4) {
let zoneId = roon_zone["zone_id"];
let hierarchy = topic_split[3].toString().toLowerCase();
let action = {};
try {
action = JSON.parse(message.toString().toLowerCase());
} catch (e) {
console.log('*** no valid JSON in message. Assume only title is passed. message: %s', message.toString() );
action.title = message.toString().toLowerCase();
}
if ( action.title ) { browse_item( zoneId, hierarchy, action ); }
} else {
if ( debug ) { console.log('*** unkown topic=% message=%s', topic , message,) ;}
}
if ( action.title ) { browse_item( zoneId, hierarchy, action ); }
} else {
if ( debug ) { console.log('*** unkown topic=% message=%s', topic , message,) ;}
}
}
});
Expand All @@ -167,6 +175,23 @@ function mqtt_get_client() {

}

function toMqttTopic(input) {
return input.replace(/[ #\+]/g,'-');
}

function zoneToMqttTopic(input) {
return toMqttTopic(input.replace(/ \+ [0-9]?/,''));
}

function roonzone_find_by_mqtttopic(zonetopic) {
for ( var zonename in roon_zones ) {
if ( toMqttTopic(zonename.replace(/ \+ [0-9]?/,'')) == toMqttTopic(zonetopic) ) {
return roon_zones[zonename];
}
}
return null;
}

function roonzone_find_by_id(zoneid) {
for ( var zonename in roon_zones ) {
if ( roon_zones[zonename]["zone_id"] === zoneid ) {
Expand All @@ -175,9 +200,10 @@ function roonzone_find_by_id(zoneid) {
}
return null;
}

function roonzone_find_output_by_name(zonename,outputname) {
for ( var output in roon_zones[zonename]["outputs"] ) {
if ( roon_zones[zonename]["outputs"][output]["display_name"].toLowerCase() === outputname.toString().toLowerCase() ) {
if ( toMqttTopic(roon_zones[zonename]["outputs"][output]["display_name"].toLowerCase()) === toMqttTopic(outputname.toString().toLowerCase()) ) {
return roon_zones[zonename]["outputs"][output];
}
}
Expand Down Expand Up @@ -396,7 +422,7 @@ function load_item_cb( err, r, opts, cb, offset) {
var roon = new RoonApi({
extension_id: 'nl.fjgalesloot.mqtt',
display_name: "MQTT Extension",
display_version: "2.1.0b",
display_version: "2.2.0b",
publisher: 'Floris Jan Galesloot',
email: '[email protected]',
website: 'https://github.com/fjgalesloot/roon-extension-mqtt',
Expand All @@ -414,7 +440,7 @@ var roon = new RoonApi({
var zoneid=data[zoneevent][zoneindex];
zonename = roonzone_find_by_id(zoneid);
if ( debug ) { console.log('*** removed zone with id %s and name %s', zoneid, zonename); }
mqtt_publish_JSON( mysettings.mqttroot + '/'+zonename, mqtt_client, { 'state' : 'removed' });
mqtt_publish_JSON( mysettings.mqttroot + '/'+ zoneToMqttTopic(zonename), mqtt_client, { 'state' : 'removed' });
delete roon_zones[zonename];
}
} else {
Expand All @@ -424,7 +450,6 @@ var roon = new RoonApi({
var zonename = zonedata.display_name || roonzone_find_by_id(zonedata.zone_id);
//var regex = '';
if ( zonename ) {
zonename = zonename.replace(/ \+.*/,'');
if ( zoneevent !='zones_seek_changed' ) {
// zones_seek_changed only passes seek/queue position. Do not refresh zone cache
roon_zones[zonename] = JSON.parse(JSON.stringify(zonedata));
Expand All @@ -433,7 +458,7 @@ var roon = new RoonApi({
roon_zones[zonename].seek_position = zonedata.seek_position;
}
if ( trace ) { console.log('*** publising(if needed) to zone %s: %s', zonename, JSON.stringify(zonedata)); }
mqtt_publish_JSON( mysettings.mqttroot + '/'+zonename, mqtt_client, zonedata);
mqtt_publish_JSON( mysettings.mqttroot + '/'+ zoneToMqttTopic(zonename), mqtt_client, zonedata);
}
}
}
Expand Down Expand Up @@ -479,6 +504,9 @@ if ( typeof mysettings.mqttport === 'undefined' ) {
if ( typeof mysettings.mqttroot === 'undefined' ) {
mysettings.mqttroot = 'roon';
saveDefaultSetting = true;
} else if ( mysettings.mqttroot != toMqttTopic(mysettings.mqttroot) ) {
mysettings.mqttroot = toMqttTopic(mysettings.mqttroot);
saveDefaultSetting = true;
}
if ( typeof mysettings.tls_rejectUnauthorized === 'undefined' ) {
mysettings.tls_rejectUnauthorized = false;
Expand Down

0 comments on commit f384572

Please sign in to comment.