Struggling to make a simple Play app.js using API

(Matthew Clegg) #7

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.

(Matthew Clegg) #8

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

(Ludovic Boyer) #9

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.

(Ludovic Boyer) #10

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

(Matthew Clegg) #11

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.

(Ludovic Boyer) #12

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”.

(Matthew Clegg) #13

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

(Ludovic Boyer) #14

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

(Ludovic Boyer) #15

@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

(Matthew Clegg) #16

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

(Ludovic Boyer) #17

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”.

(Matthew Clegg) #18

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

(Ludovic Boyer) #19

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

(Ludovic Boyer) #20

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

(Bastian) #21

Hi @Ludovic_Boyer,

As far as I know, you should not put your calls inside core_paired.
This is called constantly, and used mainly to update the zone list.

What I did was I created an http api. Then you can create your app using any language you like and have them call your apis.

–bastian

(Ludovic Boyer) #22

Hi @St0g1e,

Thanks for your answer. I know that I should not use calls inside core_paired. I did this because of my serious lack of knowledge of Java language.

I used a trick to use run a play/pause only once inside core_paired in post 20.

However, although it works great when I run the app.js directly from the Terminal, it doesn’t work at all if try to run the app.js from OpenHab, as I have to validate the API in Roon each time OpenHab runs the app.js.

I will try your and @Matthew_Clegg’s suggestion to use an http API.

Cheers,

Ludovic

(Ludovic Boyer) #23

Hi,

I tried using an http API by following the instructions given in Control Roon from the iOS Notification Center as a widget, and it works much better than what I did.

I can safely say now that my solution is wrong, or at most, not optimum.

Cheers,

Ludovic

1 Like
(Bastian) #24

Hi @Ludovic_Boyer,

I have uploaded my APIs that can be called by http at:

I have created several webpages that calls these apis (player, browser, and timer).
I have created a swift project for iPhone using these APIs as well as created widget using the iOS’ workflow app.

Please note that the webpages are just test pages to make sure the API calls are working. I have not created a good looking page or have error checking.

Hopefully it’ll help you creating your app.

–bastian

3 Likes
(Ludovic Boyer) #25

Hi @St0g1e,

Many thanks for sharing!

I followed the instructions from your Readme file and the trials I made using your test webpages impressed me!

I will try to have a closer look to adapt it to OpenHab when I will have time.

Thanks again,

Ludovic

(Bastian) #26

Hi @Ludovic_Boyer,

Cool…
Let me know if you have any questions.
By the way I have updated the repository to remove the get_image hack.

Bastian