[hyperion] Script for API Request instances >0

  • Platform information:
    • Hardware: Synology DS920+
    • OS: Docker on Synology DSM (BusyBox)
    • openHAB version: Docker stable 3.3
  • Issue of the topic: Scripting API calls for instances !=0

Since the official hyperion ng binding does’nt support LED hardware instances other then 0 i have to call the web rtc API. My setup is 5x WLED Quinled Dig Uno with SK6812 strips. One around the TV, the other behind every 5.1 speaker for a nice indirect light.
My problem is when starting or stopping hyperion ambilight only instance 0 (behind my TV) gets off. The other “sattelites” show the edged of the TV since hyperion is structured in a way that every instance has its own feature set.
The binding for hyperion ng is quite nice but is only able to talk to instance 0.
It cost me already hours scripting javascript or DSL so help would be much appreciated.
In general i “simply” would like to call the API twice:

  1. Switch to the device {"command":"instance","subcommand":"switchTo","instance":1}
  2. Turn off LED device {"command":"componentstate","componentstate":"component":"LEDDEVICE","state":false}
  3. Optional: Set up API key for security purposes

Here are a few failures i alredy made trying to simply calling the API and getting a serverinfo:

  1. with XMLHttpRequest
    not possible because any require() throws an error
var XMLHttpRequest = require('xhr2');
var xhr = new XMLHttpRequest();

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
     if (this.readyState == 4 && this.status == 200) {
         alert(this.responseText);
     }
};
xhttp.open("POST", "http://192.168.0.5:8090/json-rpc", true);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send("{command:serverinfo,tan:1}");
  1. fetch
    same problem, fetch seems to be not supported
const userAction = async () => {
const response = await fetch('http://192.168.0.5:8090/json-rpc', {
  method: 'POST',
  body: {"command":"serverinfo","tan":1}
  headers: {'Content-Type': 'application/json'}
});
const myJson = await response.json();
logger.info(myJson);
}
  1. The official way from Actions | openHAB
    also not working
var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var url = "http://192.168.0.5:8090/json-rpc"
var content = "{command:serverinfo,tan:1}"
contentType = "Content-type = application/json"
var output = sendHttpPostRequest(url, contentType, content, 1000)
logger.info("output = "+output);

Could somone PLEASE provide me a working snippet i can work with?

I assume this is part of a ECMAScript 2021 (the JS Scripting add-on) rule? If so you need to use npm to install any third party libraries at $OH_CONF/automation/js/node_modules to require them in a rule.

If you are using the ECMAScript 5.1, well ECMA didn’t support require in versions that old.

I’m not certain that await works in OH ECMAScript 2021 rules. There’s some limitations in place in GraalVM which is how the Java program openHAB is able to run JavaScript in the first place. In short, there is no loop.

Not working in what way? Were any logs produced?

Thanks for your suggestions.
Since i’m running OH in Docker i don’t want install any dependencys if i’m not forced to.
I’ll keep the ECMA Version in mind since there seems to be big differences. Do you recomment only using 2021 or are there reasons not moving forward this path? I think in future OH releases the scripts don’t have to be migrated when i do so.
I’m making progress so far but with the built in HTTP.sendHttpPostRequest method. I was getting the following error but could already solve it by myself.
[internal.handler.ScriptActionHandler] - Script execution of rule with UID 'b581e375b3' failed: ReferenceError: "HTTP" is not defined in <eval>
I did’nt know i had to load a Java function in JS. A simple var HTTP = Java.type("org.openhab.core.model.script.actions.HTTP") helped.
When i have finished my automation i’ll share it since it takes me hours developing a solution.

That doesn’t matter. You need to be mounting your configs to OH as a volume into the container anyway. npm modules get installed to one of OH’s config folders. So you don’t really need to install anything into the container except that very first time you need to run npm, and even then you could potentially install npm on the host instead of inside the container. That’s how I do it.

A little over a decade’s worth of development separates the two. ECMAScript 5.1 was released in June of 2011.

If you are just getting started you may was well start with the modern version. There are lots of nice language features and the built in openHAB helper library is much better.

But there is an add-on that will let openHAB continue to run Nashorn (the name of the ECMAScript 5.1 engine we are using) for the foreseeable future. So you are unlikely to need to migrate anytime soon.

So, that’s actually going down yet another different implementation using the Java libraries.

If you want to use the sendHttpPostRequest Action (or do just about anything else when interacting with openHAB from an ECMAScript 2021 rule) see the JS Scripting reference guide. For the HTTP Actions in particular: JavaScript Scripting - Automation | openHAB

Notice the actions.HTTP in front of the sendHttpPostRequest call.

Thats pretty helpful! Thanks!
Now i removed the last two Java libraries and running only implemented commands. Very nice!
In a test ECMAScript-2021 i logged only the item state of my switch item but getting this error:
Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.
Script:
console.log("State of HyperionAllOnOff: ", items.getItem("HyperionAllOnOff").state);

FINALLY! I got a working script for dealing with different instances in hyperion for anyone who is interested. It listens on an switch item state so in the “When” section i defined two Triggers, one for ON and one for OFF. It is a JavaScript ECMAScript-2021:

configuration: {}
triggers:
  - id: "1"
    configuration:
      command: ON
      itemName: HyperionAllOnOff
    type: core.ItemCommandTrigger
  - id: "2"
    configuration:
      command: OFF
      itemName: HyperionAllOnOff
    type: core.ItemCommandTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var switchstate = items.getItem("HyperionAllOnOff").state

        if (switchstate == "ON"){
          var state = true
        }

        else if (switchstate == "OFF"){
          var state = false
        }

        var url = "http://<HYPERION_IP:PORT>/json-rpc"
        var contentType = "application/json"

        var switchinstance = JSON.stringify({
          command: 'componentstate',
          componentstate: {
            component: 'LEDDEVICE',
            state: state
          }
        })

        for(instance=0;instance<5;instance++){
          var switchInstance = JSON.stringify({
            command: 'instance',
            subcommand: 'switchTo',
            instance: instance
          })
          switchrequest = actions.HTTP.sendHttpPostRequest(url, contentType, switchInstance, 1000)
          ledhardwarerequest = actions.HTTP.sendHttpPostRequest(url, contentType, switchinstance, 1000)
          
          console.log(JSON.parse(switchrequest).info.instance+' : '+JSON.parse(ledhardwarerequest).success)
        }
    type: script.ScriptAction

Any comments or suggestions highly appretiated!

Glad you got it working.

Is the Item that triggers the rule HyperionAllOnOff? If so assigning that to switchstate is unneeded. Just use event.newItemState. When posing an example rule, it helps to click on the code tab and post the full rule se we can see the triggers, conditions and actions too.

However, because you are telling an external device to do something, you should probably trigger the rule by received command instead. Then you’d use event.itemCommand.

You don’t need that first if statement. You can use instead

    ...
    component: 'LEDDEVICE'
    state: (switchstate == "ON")
    ...

You are setting yourself up for confusion later on down the road by having two different variables with different purposes, one named switchinstance and the other named switchInstance. Be more explicit and clear with the variable naming.

Stylistically, you probably only want instance to be scoped to the for loop so use let instance = 0;.

Since you never do anything with switchrequest and ledhardwarerequest just make the calls to sendHttpPostRequest and ignore the returned values.

If you ever want to control these individually instead of all four as a Group I recommend:

  1. Make HyperionAllOnOff be a Group with a type of Switch.
  2. Create an Item for each of the four devices and make them all a part of the Group from 1. Use a name like: HyperionLED_X where X is 1 to 4.
  3. Create one rule that triggers when Member of HyperionAllOnOff received command
var url = 'http://<HYPERION-IP>/json-rpc';
var contentType = 'application/json';
var deviceNum = event.itemName.split('_')[1];

var switchPayload = JSON.stringify( {
    command: 'componentstate',
    componentstate: {
        component: 'LEDDEVICE',
        state: (event.itemCommand.toString() == 'ON')
    }
});

var ledhwPayload = JSON.stringify( {
    command: 'instance',
    subcommand: 'switchTo',
    instance: deviceNum
});

actions.HTTP.sendHttpPostRequest(url, contentType, switchPayload, 1000);
actions.HTTP.sendHttpPostRequest(url, contentType, ledhwPayload, 1000);

To control one individual device send an ON or OFF command to that device’s Item (e.g. HyperionLED_2). To control all the devices, send an ON or OFF command to the Group. The Group will forward the command to all members of the Group. Each command to the members of the Group will trigger the rule, once for each of the four Items.

The rule parses the device number out of the Item’s name and sends the request as required.

If you want to know when the LEDs are ON or OFF even when controlled outside of OH you are probably best using the HTTP binding instead of the rule above. You can configure it to poll the URL for changes in state of the device.

Wow so many great suggestions! Thanks a lot! Having a lot of fun tinkering and making a lot of progress in JS since i set up this post. As a JS noob and only bash experienced this is a whole new world for me.
I updated my post so all of the code is included as you suggested. Good point.
The tip with the switchstate is good but what happens if it is neither true or false for some reason? (speaking of null or undefined for example). This would exec the script but the API call would fail right?

component: 'LEDDEVICE'
state: (switchstate == "ON")

You were right with switchrequest and ledhardwarerequest as i didn’t paste my logging line in my script. I’m at the beginning with error handling trying to do a lot better on this.

Great idea grouping single items together! I’ll do that and post an update of my script when i get it working. Another benefit is that the for loop in my current setup brings a bit of a delay for the last api calls. In reality it won’t make much difference but notable if you look close. A group update makes parallel calls right?

It can never be anything but true or false. If switchstate is anything but “ON” (“OFF”, “NULL”, “UNDEF”, null, undefined) that expression will return false. If you were to change to use event.itemCommand and change the trigger to received command the then event.itemCommand can only be ON or OFF. Nothing else can be sent as a command and the rule won’t trigger if there is no command.

I don’t expect that with change with the Group solution. The problem is the HTTP calls have to made in sequence in both cases and each call takes some time to complete.

Yes but only one instance of a given rule can run at a time. So your rule triggers will queue up and run in sequence. If you want it to work in parallel you’ll have to use the HTTP binding instead of a rule.

Sounds logic. If it’s not “ON” (true) its false.
I made a lot of progress due to your awesome snippet. It had a small bug in the deviceNum variable since the API wanted to have an integer and JSON.stringify() intepretes the split() result as a string.
My final and much more simple solution as my initial script is now as follows:

triggers:
  - id: "1"
    configuration:
      groupName: HyperionAll
    type: core.GroupCommandTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var url = 'http://192.168.0.5:8090/json-rpc';
        var contentType = 'application/json';
        var deviceNum = parseInt(event.itemName.split('_')[1]);

        var ledhwPayload = JSON.stringify( {
            command: 'instance',
            subcommand: 'switchTo',
            instance: deviceNum
        });

        var switchPayload = JSON.stringify( {
            command: 'componentstate',
            componentstate: {
              component: 'LEDDEVICE',
              state: (event.itemCommand.toString() == 'ON')
            }
        });

        actions.HTTP.sendHttpPostRequest(url, contentType, ledhwPayload, 1000);
        actions.HTTP.sendHttpPostRequest(url, contentType, switchPayload, 1000);

    type: script.ScriptAction

Thank you so much for your help Rich!

Since I don’t use Hyperion I am not sure exactly what your doing but the WLED project supports the Live override feature that allows you to interrupt the Hyperion use and fall back to using the segments as normal descrete light globes. The wled binding has this LOR feature implemented and will address each segment separately.