Transport API error and questions

Hi @danny

I found an error in the zone_by_zone_id function in the Transport API.
Please find below the fix that is working.

RoonApiTransport.prototype.zone_by_zone_id = function(zone_id) {
    if (!this._zones) return null;
//    for (var x in this._zones) if (x == zone_id) return this._zones[x];
    for (var x in this._zones) if (this._zones[x].zone_id == zone_id) return this._zones[x];
    return null;
}

I also have several questions on some of the Transport APIs.

  1. Would it be possible to return the zone list on get_zones

  2. For Control’s 'next" and ‘previous’ calls, is it possible to get the updated zone information as the return? The reason i’m asking for this is because currently I have to do settimeout for 2 seconds before calling the zone_by_zone_id function to ensure that the information is updated. This is especially long if I want to play the previous Song as I have to call this function twice (hence, 4 seconds delay in UI update)

  3. For zone_by_zone_id and zone_by_output_id, is it possible to get the values directly as opposed to traversing the zone list?

  4. I can’t find the correct api to get the image once I have the image_key, could you point me on how to do this?

Thank you very much for your help.
Bastian

P.S. For future questions on the APIs, is it better to post it here or at the GitHub directly?

can you give me a github pull request for this?

isnt that what it does? .get_zones((zones) => { ... do stuff with zones list ... })

if you want access to the zones list without having to go directly to the roon core, you must subscribe to the zones listing using .subscribe_zones()

are you using the subscribe on the zone information? they should give you the latest info in realtime. zero delays needed.

I don’t understand the question here… if you use .subscribe_zones(), it will listen and keep the zone list in the client, so these functions should return good and valid values.

I just pushed comments to node-roon-api-image/lib.js – the docs arent generated, but you can see docs inside the lib.js

here is better, since eventually, others can help reply too!

at github, bugs in the form of issues + pull requests are very welcome.

Hi @danny, thank you for your reply.

I have created an Issue for this in GitHub.

I am actually using .subscribez_zones, but since the controls’ “next” and “previous” function does not give me the updated zone information (please refer to the questions further below), i am trying to get the data from the core right after the call.

Here’s my take on the get_zones call that does not return the list of zones:

exports.listZones = function(req, res) {
  var current_zones = core.services.RoonApiTransport.get_zones();
  res.send({
//    "zones": zones
    "zones": current_zones
  })
};

This is regarding Control’s “next” and “previous” calls as well as “zone_by_zone_id” and “zone_by_output_id” combined.

My question here is because i find that the control’s “next” and “previous” does not return the updated zone information, I would like to know if there is a way to get the zone’s data directly from the core (either by zone_id or output_id)

Please take a look at the code that calls “next” below.

First i’m calling Control’s next function.
Then
outputting the zone information by:

  1. data updated by subscribe_zone
  2. return from zone_by_zone_id

I have tried both with and without timeout.
for both ways (subscribe_zone and zone_by_zone_id calls) if I don’t use the timeout, the zone returned has not been updated, i need between 1500-2000 of timeout to have the zone updated.

here’s where i was asking whether it is possible to have a call for zone_by_zone_id or zone_by_output_id directly to the core for the updated information so that i don’t have to wait for subscribe_zone to finish its update.

exports.next = function(req, res) {
  core.services.RoonApiTransport.control(zones[req.query['zoneId']], 'next');

//  NO Timeout. zone information has not been updated
//  res.send({
//      "zone": zones[req.query['zoneId']]  // from .subscribe_zones
//    "zone": core.services.RoonApiTransport.zone_by_zone_id(req.query['zoneId'])
//  })

//  WITH Timeout. zone information is updated
    setTimeout(function(){
       res.send({
//         "zone": zones[req.query['zoneId']] // from .subscribe_zones
         "zone": core.services.RoonApiTransport.zone_by_zone_id(req.query['zoneId'])
       })
    }, 2000);
};

Thanks Danny, i will try this out and let you know.

Once again, thank you so much for answering my questions.

@St0g1e - first of all, thank you very much for your work on this API at https://github.com/st0g1e/roon-extension-ws-player. It has been incredibly helpful as I was working through this.

But back on topic. If I read your intent of using “get_zones()”, you are looking for a way to have a list of active zones that updates automatically as zones are added and removed. If that is correct, then this is something I was also looking for.

Below is what I came up with using a different approach than you had in your code. The resulting variables “zoneList” and “zoneStatus” are JSON arrays that can be fed to a web socket channels/subscriptions/feeds whatever.

The array “zoneList” always has the current list of the zones that Roon knows about and it updates as zones are added and removed. It is an array of “zone_id”, “output_id”, and “display_name”.

The array “zoneStatus” always has the current status of the zones that Roon knows about and it too updates as zones are changed, added and removed. It is the full array that is provided by the Roon API.

Is this helpful?

(NOTE: this is my first publicly posted javascript. If you have suggestions for improvement, let me know.)

// Setup general variables
const listenPort = 8080;

var core;
var zoneStatus = [];
var zoneList = [];

// Setup Express
var express = require('express');
var http = require('http');

var app = express();
app.use(express.static('public'));

app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});

// Setup Socket IO
var server = http.createServer(app);
var io = require('socket.io').listen(server);

server.listen(listenPort, function() {
    console.log('Listening on port ' + listenPort);
})


// Setup Roon
var RoonApi          = require("node-roon-api");
var RoonApiStatus    = require("node-roon-api-status");
var RoonApiTransport = require("node-roon-api-transport");

var roon = new RoonApi({
    extension_id:        'com.pluggemi.roon.ws.api',
    display_name:        "Web Socket API",
    display_version:     "1.0.0",
    publisher:           'Mike Plugge',
    email:               'masked',
    website:             'masked',

    core_paired: function(core_) {
        core = core_;

        transport = core_.services.RoonApiTransport;

        transport.subscribe_zones(function(response, data){
            if (response == "Subscribed") {
                for ( x in data.zones ) {
                    var zone_id = data.zones[x].zone_id;
                    var display_name = data.zones[x].display_name;

                    for (y in data.zones[x].outputs){
                        var output_id = data.zones[x].outputs[y].output_id;
                    }

                    item = {};
                    item ["zone_id"] = zone_id;
                    item ["output_id"] = output_id;
                    item ["display_name"] = display_name;

                    zoneList.push(item);

                    zoneStatus.push(data.zones[x])
                }

                io.emit("zoneList", zoneList);
                io.emit("zoneStatus", zoneStatus);
           }
           else if (response == "Changed") {
                if (data.zones_added){
                    for ( x in data.zones_added ) {
                        var zone_id = data.zones_added[x].zone_id;
                        var display_name = data.zones_added[x].display_name;

                        for (y in data.zones_added[x].outputs){
                            var output_id = data.zones_added[x].outputs[y].output_id;
                        }

                        item = {};
                        item ["zone_id"] = zone_id;
                        item ["output_id"] = output_id;
                        item ["display_name"] = display_name;

                        zoneList.push(item);
                        zoneStatus.push(data.zones_added[x])
                    }
                    io.emit("zoneList", zoneList);
                    io.emit("zoneStatus", zoneStatus);
                }
                else if (data.zones_removed){
                    for (x in data.zones_removed) {
                        zoneList = zoneList.filter(function(zone){
                            return zone.zone_id != data.zones_removed[x];
                        });

                        zoneStatus = zoneStatus.filter(function(zone){
                            return zone.zone_id != data.zones_removed[x];
                        });
                    }
                    io.emit("zoneList", zoneList);
                    io.emit("zoneStatus", zoneStatus);
                }
                else if (data.zones_changed){
                    for (x in data.zones_changed){
                        for (y in zoneStatus){
                            if (zoneStatus[y].zone_id == data.zones_changed[x].zone_id){
                                zoneStatus[y] = data.zones_changed[x];
                            }
                        }
                    }
                    io.emit("zoneStatus", zoneStatus);
                }
                else {
                    console.log("Unknown transport response: " + response + " : " + data);
                }
            }
        });
    },

    core_unpaired: function(core) {}
});

var svc_status = new RoonApiStatus(roon);

roon.init_services({
    required_services: [ RoonApiTransport],
    provided_services: [ svc_status ]
});

svc_status.set_status("Extenstion enabled", false);

roon.start_discovery();

// Web Socket
io.on('connection', function(socket){
    io.emit("zoneList", zoneList);
    io.emit("zoneStatus", zoneStatus);
});

// Web Routes
app.get('/', function(req, res){
    res.sendFile(__dirname + '/public/index.html');
});

Hi @Mike_Plugge,

Thank you for your suggestion, this is definitely one way of doing it.
You send the updated zoneList whenever there is addition/deletion and I think it will make the client code cleaner.
The way I check it on my client upon receiving zone update is I check whether the current zone list has the same length as the new one and act on it (update the dropdown list)

However, I have separated the Control (next, prev, and play/pause) with the UI update.
For the control, the client just sends the command without waiting for the zones to be updated. The UI will update itself when/once there is a change in the zone.

You seem to be using array for zoneList and zoneStatus, I found out later that it was my mistake and hence the creation of this thread. It would be better to use hash for these variables as there would be less loops down the line to search for a particular zone from the list, for example. zone = zoneList[zoneId] instead of looping and to find which zoneList[iteration].zoneId == zoneId.

Also, I haven’t looked into outputs in detail, but I seem to recall that for each zone, you can have multiple outputs. You seem to only get the last one.

Again, thank you for your suggestion. I will look into sending the zoneList on add/remove to make the clients’ workflow cleaner.

Thank you,
Bastian

Thanks @St0g1e!

That was for an example app. For my full player application, I am using your same code for the control code. I liked the simplicity of it! All that I added was a goStop on the player side and a socket.on(‘goStop’) section to handle situations like streaming radio where pause is not available. I typically just use my own internal GitLab server, but I just opened a GitHub account so you could see the full code.

Good catch on the possibility of having multiple output_ids. I will have to look into that. But at the moment, with the 2 zones that I have, they each only have a single output_id, so I figured that was the norm…

I avoided a lot of the zoneList[i].zoneId situations by using a global variable for the current zone (curZone) and having most of the client code parse only data that was selected. This variable is set based on the selectZone function using the curZoneID variable, which is also saved and read from a cookie for cross session persistence.

Note - in the player.js equivilent (/public/js/site.js), I am using jQuery, js.cookie.js for cookie management, and jquery.simplemarquee.js to auto scroll very long song/artist/albums names. All of those libraries are included in the GitHub posted version. Internally I use a self hosted CDN.

Here is the GitHub link, but please note that the volume button doesn’t do much right now. It is a low priority for me because both of my zones have a fixed volume, so I really can’t test it. But I am pleased with the look and feel with 4 themes to select from the settings/cogs button on the bottom left.