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

1 Like

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