Struggling to make a simple Play app.js using API

Hi all,

I have been enjoying Roon for a year now, and I think I will enjoy it more now that we can interact with it using APIs.
However, I am struggling to send a “playpause” to one output (HQPlayer) command to my Roon Core.
Can you please post an example code allowing me to do so?

Many thanks in advance an sorry for such a basic question.

Cheers,

Ludovic

Deleted xxx

Not sure if you asking to control HQPlayer from Roon. In the meantime you may find this interesting

1 Like

Hi,

Many thanks for your answer and thanks for the information.

I would like to control Roon from OpenHab. In fact, I was thinking to run the appropriate “app.js” (the one to start or stop the music in a specific zone) from OpenHab, depending on some rules. For instance, when I want to relax, I would like to send a command to OpenHab (this I can do). In response, OpenHab sends a command to Roon to start the music from a specific playlist in my living room, for instance (this I cannot do).

My first step is to learn how to start or stop playing music in a specific zone from an “app.js”. When I will be comfortable with this, I will try to see if I can select a playlist, then start the music.

If successful, I would be happy to post the resulting code in this forum.

Cheers,

Ludovic

Ludovic,

I have been looking into the same thing (but probably want a little more functionality) have a look at this thread which sets up a really simple REST interface for Roon

You will need to change the default zone in the code, but from there play/stop/next etc can be triggered by sending a HTTP get request.

Then it is as simple as using the HTTP binding in Openhab. See here:

Thanks Matthew,

I will try it.
May I ask you why go the HTTP binding, instead of directly executing a .js document? It seems to add a layer between OpenHAB and Roon.

Cheers,

Ludovic

Purely because this was available and I didn’t have to develop the interface from scratch. My plans are slightly wider so still need to some work but for just playing a known zone this would give you a quick fix without massive development.

On top of this to interact directly with node.js i think you would either have to:

  • Write a binding
  • Write a bunch of .js apps for each function and use the exec binding

This was just on the whole easier for now until a binding is developed, but that could be never as roon is a pretty niche use case

Actually, my plan was to try to use the exec binding with a bunch of .js apps or, if possible, to write a generic .js, such as yours using the “case” statement and then run it using the exec binding with arguments.

Out of curiosity, I tried the following code, based on the link you provided earlier @Matthew_Clegg .

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

var zones = [];
var defaultZone="HQPlayer";
var transport;

var roon = new RoonApi({
    extension_id:        'com.restserver.test',
    display_name:        "Roon REST Server",
    display_version:     "0.0.1",
    publisher:           'Walter Wego',
    email:               'tbd',
    website:             'tbd',
    
    core_paired: function(core) {
        transport = core.services.RoonApiTransport;
        // get available zones
        transport.subscribe_zones(function(cmd, data) {
            if (cmd == "Subscribed") {
                console.log("Subscribed:", data);
                zones = data.zones;
            } else if (cmd == "Changed") {
                if ("zones_added" in data) {
                    console.log("zones_added:", data.zones_added);
                    for (var item in data.zones_added) {
                        if (!getZoneByName(data.zones_added[item].display_name)) {              
                            zones.push(data.zones_added[item]);
                        }
                    } 
                }
            } else {
                console.log("unhandled transport cmd",cmd,":",data);
            }
       });
    },

    core_unpaired: function(core) {
        console.log("unpaired core", core.display_name);
    }
});

var svc_status = new RoonApiStatus(roon);

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

svc_status.set_status("Running", false);

roon.start_discovery();

console.log("play in zone");
transport.control("HQPlayer", "play"); 

Unfortunately, when I launched the .js app using “node .”, it returned the following error message:

play in zone
/home/pi/roon/roon-extension-essai-play/app.js:66
transport.control("HQPlayer", "play");
         ^

TypeError: Cannot read property 'control' of undefined
    at Object.<anonymous> (/home/pi/roon/roon-extension-essai-play/app.js:66:10)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
    at run (bootstrap_node.js:393:7)
    at startup (bootstrap_node.js:150:9)
    at bootstrap_node.js:508:3

It seems that the “transport” is not defined. I don’t understand why as I though transport was defined, first as a variable with “var transport”, then related to the Roon core using “transport = core.services.RoonApiTransport;” in “core_paired: function(core)”. Is the relation of “transport” in “function(core)” only local?

note: when I remove “transport.control("HQPlayer", "play");” from the .js app, it is running but, obviously, nothing happens.

Cheers,

Ludovic

I didn’t write this originally but i don’t think you can address the zone by name in transport. The original code passed a variable “zone” to the transport function. This it looks to me was set earlier on in the script. (i suspect it is the zone I’d) but would have to do some more digging to be sure. What i would do is copy the section that sets the zone variable and then pass that to the transport function.

Thanks for the information.

You are right about the fact that I cannot address the zone by in “transport”. However, in this case, if I understand correctly the error message, “transport” is not linked to the “transport” variable in “core_paired: function(core)”. I think the latter is only local.

To check this, I tried the following:

At the beginning to the app.js file, I created variable with an assigned value “var test="XXXX"”. Then, in “core_paired: function(core)”, I assigned another value to test “test="YYYY"”.
Finally, after the line stating “roon.start_discovery();”, I wrote “console.log(test)”.
When I launched “app.'s” the output was “XXXX”.

Ok another thought, dis you install the dependencies as documented here:

Hi,

Based on what I have deduced from the preceding post, I modified the above code to the following:

//Launch the app as follow: node app.js zone command
//for instance: node app.js LivingRoom play

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

var zones=[];
var zone;
var name=process.argv[2];
var ctrlcmd=process.argv[3];

var roon = new RoonApi({
    extension_id:        'com.elvis.test',
    display_name:        "Elvis's First Roon API Test",
    display_version:     "1.0.0",
    publisher:           'Elvis Presley',
    email:               'elvis@presley.com',
    website:             'https://github.com/elvispresley/roon-extension-test',

     core_paired: function(core) {
        transport = core.services.RoonApiTransport;

        // get available zones
        transport.subscribe_zones(function(cmd, data) {
            if (cmd == "Subscribed") {
                console.log("Subscribed:", data);
                zones = data.zones;
            } else if (cmd == "Changed") {
                if ("zones_added" in data) {
//                    console.log("zones_added:", data.zones_added);
                    for (var item in data.zones_added) {
                        if (!getZoneByName(data.zones_added[item].display_name)) {     $
                            zones.push(data.zones_added[item]);
                        }
                    }
                }
            } else {
                console.log("unhandled transport cmd",cmd,":",data);
            }
         zone=getZoneByName(name);


         //Send Control command to Roon
         transport.control(zone,ctrlcmd);
       });
    },
    
        core_unpaired: function(core) {
        console.log("unpaired core", core.display_name);
    }


});

var svc_status = new RoonApiStatus(roon);

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

svc_status.set_status("All is good", false);

roon.start_discovery();

// helper function to get zone by their name
function getZoneByName(name) {
    for (item in zones) {
        if (name == zones[item].display_name) {
            return zones[item];
        }
    }
    return null;
}

As the transport command “transport.control(zone,ctrlcmd);” is now in “core_paired: function(core)”, it works.

The zone id is obtained from the function “function getZoneByName(name)” assigned to the variable “zone” using “zone=getZoneByName(name);”, also located in “core_paired: function(core)”.

The zone name and the command are passed to the “app.js” this way: “node app.js zone command”.
The arguments “zone” and “command” are retrieved in the “app.js” with the following variables: “var name=process.argv[2];” and “var ctrlcmd=process.argv[3];”.

However, this code as a major drawback, as the function “core_paired: function(core)” seems to be called ever and ever, the only commands that work are “play”, “pause” and “stop”. I tried “next”, but Roon will go to the next song indefinitely.

Thus, I think this code can do the work for now, but it is far from optimum.

Does someone know how to use “transport.control(zone,ctrlcmd);” outside “core_paired: function(core)”?

Cheers,

Ludovic

@Matthew_Clegg,

Refering your post (n°13), yes, I have installed Node.js on my Raspberry pi 3. The installed version is v6.10.1. The NPM version is 3.10.10.

Cheers,

Ludovic

No I think there is a step (see link) where you have to define the dependencies in the manifest (package.json) and the install them using npm install

Yes, I created a “package.js”, then installed the dependencies with the “npm install” command.

I just double checked all the dependencies required by “app.js” are defined in “package.js”.

and when you ran npm install. It definitely installed the 3 deps?

I think so.

I ran again “npm install” after removing “config.js” and the nodexx folder. The resulting warnings are shown below:

npm WARN deprecated node-uuid@1.4.8: Use uuid module instead
roon-extension-alarm-clock@0.0.1 /home/pi/roon/roon-extension-essai-play
├─┬ node-roon-api@0.0.1  (git://github.com/roonlabs/node-roon-api.git#41f32588737858b8d3d0d1b37cc931b9687308fe)
│ ├── ip@1.1.5 
│ ├── node-uuid@1.4.8 
│ └─┬ ws@1.1.4 
│   ├── options@0.0.6 
│   └── ultron@1.0.2 
├── node-roon-api-status@1.0.0  (git://github.com/roonlabs/node-roon-api-status.git#504c918d6da267e03fbb4337befa71ca3d3c7526)
└── node-roon-api-transport@1.0.0  (git://github.com/roonlabs/node-roon-api-transport.git#92df1a84f82957cd0b0e4e46da755b61aaccfe4c)

npm WARN roon-extension-alarm-clock@0.0.1 No repository field.

From the message, it seems that the dependencies are installed.

However, I don’t understand the warning “npm WARN deprecated node-uuid@1.4.8: Use uuid module instead

Hi,

I have found a way to use “transport.control(zone,ctrlcmd);” only once although it is located in “core_paired: function(core)”. The following code from post n°14:

zone=getZoneByName(name);
//Send Control command to Roon
transport.control(zone,ctrlcmd);

has to be replaced by:

//Trick execute the control command only once
i=i+1;
if(i==1){
zone=getZoneByName(name);

//Send Control command to Roon
transport.control(zone,ctrlcmd);
//Exit the Node js once the command is sent to Roon
process.exit(1);
}

At the beginning of the “app.js” file the variable “i” must be created and assigned to the value “0” using “var i=0”.

So, it works this way:
When executing “app.js”, “core_paired: function(core)” will be executed on and on.
The first time “core_paired: function(core)” is executed, “i” will be incremented to the value “1”. In this case, the code written in the brackets of the “if” statement will be executed as “i” is equal to “1” ("if(i==1)").
The next time “core_paired: function(core)” is executed, “i” will be incremented to the value “2”. In this case the code written in the brackets of the “if” statement will not be executed as “i” is not equal to “1” anymore. And so on.

This allows me to remove the drawback from the previous code and now I can pass arguments such as “next” to “app.js”.

Furthermore, as I don’t want “app.js” to run continuously, in order to save resources from my Raspberry pi, I decided to stop “app.js” once “transport.control(zone,ctrlcmd);” is executed. This other benefit is that,this way, I think will be able to execute “app.js” directly from OpenHAB several times. I still have to try this though.
This is done using “process.exit(1);”. By doing so, the increment of “i” and the “if” statement is no longer required. I left it here so anyone can use it without “process.exit(1);”.

Finally, the complete code is

//Launch the app as follow: node app.js zone command
//for instance: node app.js LivingRoom play

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

var zones=[];
var zone;
var name=process.argv[2];
var ctrlcmd=process.argv[3];
var i=0;

var roon = new RoonApi({
    extension_id:        'com.elvis.test',
    display_name:        "Elvis's First Roon API Test",
    display_version:     "1.0.0",
    publisher:           'Elvis Presley',
    email:               'elvis@presley.com',
    website:             'https://github.com/elvispresley/roon-extension-test',

     core_paired: function(core) {
        transport = core.services.RoonApiTransport;
        // get available zones
        transport.subscribe_zones(function(cmd, data) {
            if (cmd == "Subscribed") {
                console.log("Subscribed:", data);
                zones = data.zones;
            } else if (cmd == "Changed") {
                if ("zones_added" in data) {
                    console.log("zones_added:", data.zones_added);
                    for (var item in data.zones_added) {
                        if (!getZoneByName(data.zones_added[item].display_name)) {     $
                            zones.push(data.zones_added[item]);
                        }
                    }
                }
            } else {
                console.log("unhandled transport cmd",cmd,":",data);
            }
			
			//Trick execute the control command only once
			i=i+1;
			if(i==1){
		 		zone=getZoneByName(name);

				//Send Control command to Roon
				transport.control(zone,ctrlcmd);
				//Exit the Node js once the command is sent to Roon
				process.exit(1);
         	}
         
       });
    },
    
        core_unpaired: function(core) {
        console.log("unpaired core", core.display_name);
    }


});

var svc_status = new RoonApiStatus(roon);

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

svc_status.set_status("All is good", false);

roon.start_discovery();

// helper function to get zone by their name
function getZoneByName(name) {
    for (item in zones) {
        if (name == zones[item].display_name) {
            return zones[item];
        }
    }
    return null;
}

It is far from optimum, but it does the work.
I would be happy to see a simplified code to achieve the same goal.

Many thanks @Matthew_Clegg for your help!

Cheers,

Ludovic