jpg0 / ohj

Openhab Javascript Library
Eclipse Public License 2.0
6 stars 2 forks source link

Update README.md to highlight OH 2.5 requirement #2

Open JamieTemple opened 4 years ago

JamieTemple commented 4 years ago

Hello,

So this looks like a huge improvement on the current JS library, so I thought I'd have a go this evening ... unfortunately, it looks like I can't just drop the GraalJS bundle into my addons folder - as it complains:

Unresolved requirement: Import-Package: org.openhab.core.automation.module.script; version="[2.5.0,3.0.0)" at org.eclipse.osgi.container.Module.start(Module.java:444) ~[?:?]

... I'm guessing that I can't play with things until I update to 2.5 (a job I'm saving for the xmas break :)

Jamie.

jpg0 commented 4 years ago

So the version dependency exists as I built against 2.5.x, because that was what I pulled from github :) It may well be the case that it will run on 2.4.x if it's told to try. I'm happy to rebuild and widen the accepted dependency versions if you want to give it go on 2.4 before the break?

You're also welcome to tweak it directly in the bundle jar too if you like - the versions that it wants are in META-INF/MANIFEST.MF, you'd just need to find all the occurrences of [2.5.0,3.0.0) and replace with [2.4.0,3.0.0). No guarantee that it actually works, but it it will probably tell you (& me) whether it does.

JamieTemple commented 4 years ago

So I've got a little further today ... but not quite there :(

The npm package seemed out of date, so I cloned straight from here instead.

... running on OH 2.5 now :)

problem when running scripts now - Module not found: @runtime/services

... seems to be related to osgi.js ...

... possibly related to https://github.com/openhab/openhab-core/pull/1214

Unfortunately, I don't really know where to start digging in OH core to figure out how to change things :(

jpg0 commented 4 years ago

Ah, that's because you have the latest JS, but I don't think the latest bundle jar. I've attached a zip of the jar (GH allows zips but not jars), so if you unzip it and give it a go it should work.

The actual issue is that I added a 'services' script extension to give scripts an API to lookup/register/unregister their own osgi services. Anywhere you see an import of a module @runtime/<something> it is the script trying to import something from the host/OH. This replaces the scriptExtension.importPreset(<something>) from before which just dumped all the symbols into the global namespace. Something else I should add to the docs!

org.openhab.automation.module.script.graaljs-2.5.0-SNAPSHOT.jar.zip

JamieTemple commented 4 years ago

Thanks for this - between OH2.5 & this, xmas has definitely come early! :)

I've spent a large chunk of this evening rewriting my old js rules.

Here is one of the latest - I'd really appreciate your comments, as I'm sure there will be simpler / cleaner ways of doing things:


const { getItem } = require('ohj').items;

const br_channel_BBC1_SD = 1;
const br_channel_BBC1 = 101;
const br_channel_BBC2 = 101;
const br_channel_BBC4 = 9;
const br_channel_BBC_News = 231;
const br_channel_ITV = 103;
const br_channel_ITV2 = 6;
const br_channel_C4 = 104;
const br_channel_C5 = 105;
const br_channel_Dave = 19;

const lr_channel_BBC1_SD = 101;
const lr_channel_BBC1 = 108;
const lr_channel_BBC2 = 102;
const lr_channel_BBC4 = 107;
const lr_channel_BBC_News = 601;
const lr_channel_ITV = 113;
const lr_channel_ITV2 = 115;
const lr_channel_C4 = 104;
const lr_channel_C5 = 150;
const lr_channel_Dave = 127;

with(require('ohj').fluent){

    when(item("TV_BBC_One_SD").changed().toOn())
        .then(() => changeChannel("TV_BBC_One_SD", lr_channel_BBC1_SD, br_channel_BBC1_SD));

    when(item("TV_BBC_One").changed().toOn())
        .then(() => changeChannel("TV_BBC_One", lr_channel_BBC1, br_channel_BBC1));

    when(item("TV_BBC_Two").changed().toOn())
        .then(() => changeChannel("TV_BBC_Two", lr_channel_BBC2, br_channel_BBC2));

    when(item("TV_BBC_Four").changed().toOn())
        .then(() => changeChannel("TV_BBC_Four", lr_channel_BBC4, br_channel_BBC4));

    when(item("TV_BBC_News").changed().toOn())
        .then(() => changeChannel("TV_BBC_News", lr_channel_BBC_News, br_channel_BBC_News));

    when(item("TV_Itv").changed().toOn())
        .then(() => changeChannel("TV_Itv", lr_channel_ITV, br_channel_ITV));

    when(item("TV_Itv_Two").changed().toOn())
        .then(() => changeChannel("TV_Itv_Two", lr_channel_ITV2, br_channel_ITV2));

    when(item("TV_Channel_Four").changed().toOn())
        .then(() => changeChannel("TV_Channel_Four", lr_channel_C4, br_channel_C4));

    when(item("TV_Channel_Five").changed().toOn())
        .then(() => changeChannel("TV_Channel_Five", lr_channel_C5, br_channel_C5));

    when(item("TV_Dave").changed().toOn())
        .then(() => changeChannel("TV_Dave", lr_channel_Dave, br_channel_Dave));

    when(item("Alexa_TV_POWERTOGGLE").receivedUpdate())
        .then(() => {
            if (IsBedroom()) {
                if (getItem("Alexa_TV_POWERTOGGLE").state == "OFF") {
                    getItem("Harmony_Bedroom_Activity").sendCommand("PowerOff");
                } else {
                    getItem("Harmony_Bedroom_Activity").sendCommand("YouView");
                }
            } else {
                if (getItem("Alexa_TV_POWERTOGGLE").state == "OFF") {
                    getItem("Harmony_LivingRoom_Activity").sendCommand("PowerOff");
                } else {
                    getItem("Harmony_LivingRoom_Activity").sendCommand("Watch TV");
                }
            }
        });

    when(item("Alexa_TV_Mute").receivedUpdate())
        .then(send("Mute").toItem(ButtonPress()));

    when(item("Alexa_TV_Volume").receivedUpdate())
        .then(() => {
            var harmonyItem = getItem(ButtonPress());
            var volumeMessage = getItem("Alexa_TV_Volume").state == "10" ? "VolumeUp" : "VolumeDown";

            harmonyItem.sendCommand(volumeMessage);
            java.lang.Thread.sleep(500);
            harmonyItem.sendCommand(volumeMessage);
        });

    when(item("Alexa_TV_Activity").receivedUpdate())
        .then(() => {
            var harmonyItem = getItem(IsBedroom() ? "Harmony_Bedroom_Activity" : "Harmony_LivingRoom_Activity");

            switch(getItem("Alexa_TV_Activity").state) {

                case 'TV':      // TV selects the default TV viewing activity.
                    harmonyItem.sendCommand(IsBedroom() ? "YouView" : "Watch TV");
                    break;

                case 'TUNER':   // TUNER selects the TV's built-in Freeview TV tuner.
                    harmonyItem.sendCommand(IsBedroom() ? "TV" : "Freeview");
                    break;

                case 'SMARTCAST':  // The third way...
                    harmonyItem.sendCommand(IsBedroom() ? "Fire TV" : "Watch TV");
                    break;
            }
        });

    when(item("Scene_Good_Morning").changed().toOn())
        .then(() => {
            // Bedroom TV power on - Switch to BBC 1
            getItem("Bedoom_TV_ChannelNumber").sendCommand(br_channel_BBC1_SD);

            // Kitchen Projector & soundbar on
            //sendCommand("Broadlink_Projector_POWERTOGGLE", "ON");
            //sendCommand("Broadlink_Soundbar_POWERTOGGLE", "ON");

            // Kitchen Echo to BBC IPlayer

            // Bathroom echo to BBC Radio 4
            //sendCommand("Echo_Office_PlayMusicCommand", "BBC radio four");

            getItem("Scene_Good_Morning").postUpdate("OFF");
        });
}

/**
 * .
 * 
 * @param {string} sceneItem - The Alexa scene triggered (to reset)
 * @param {int} livingRoomChannelNumber - The channel number if living room tv.
 * @param {int} bedroomChannelNumber - The channel number if bedroom tv.
 */
function changeChannel(sceneItem, livingRoomChannelNumber, bedroomChannelNumber) {
    if (IsBedroom()) {
        getItem("Bedoom_TV_ChannelNumber").sendCommand(bedroomChannelNumber);
    } else {
        getItem("LivingRoom_TV_ChannelNumber").sendCommand(livingRoomChannelNumber);
    }
    getItem(sceneItem).postUpdate("OFF");
}

function IsBedroom() {
    return getItem("Echo_LastUsed_DeviceName").state == "Echo_Bedroom_TTS";
}

function ButtonPress() {
    return IsBedroom() ? "Harmony_BedroomTv_ButtonPress" : "Harmony_Soundbar";
}

... also, is there any object context that can be passed into the then - e.g. then((someContext) => { someContext.foo; })

Many thanks for all your work - I'll spend a while familiarizing myself with things some more - I'd be happy to lend a hand going forward - Jamie.

jpg0 commented 4 years ago

Awesome! Great to see that you've got it working! Hopefully those rules are a lot more concise than they were in the old rules engine!

As for improving further, things I'd suggest:

JamieTemple commented 4 years ago

Awesome! Great to see that you've got it working! Hopefully those rules are a lot more concise than they were in the old rules engine!

... that's very generous of you, describing it as the "old rules engine" ... not what I'd call it ;)

As for improving further, things I'd suggest:

  • For the channel changing parts, you can always wrap the rule creation in it's own function, so that you don't need to repeat the when(...).then(...), you could just take the 3 args: for example: wireUpChannel("TV_BBC_One_SD", lr_channel_BBC1_SD, br_channel_BBC1_SD). An alternative, possibly simpler is to define the consts in an array, or object properties (of the 3 args for each channel in your case), then loop through them setting up each channel rule.

Yes - a good idea - I've been trying to get my old rules to run first - optimising comes next, once I know what I'm doing :)

  • There actually is an object context that is passed through, but it's pretty anemic so far and I don't think that it supports adding things. It's used to pass through the receivedCommand so that you can do things like: when(item('foo').receivedCommand()).then(sendIt().toItem('bar')) (the command is passed as 'it'). I can see the benefit of allowing adding to it though, although I haven't yet had the need to do it myself. If you have any specific examples of what you think would be worth supported, or even want to submit a PR, that would be great!

Here is another of my rules - which is now even simpler with the use of the receivedCommand:

const { getItem } = require('ohj').items;

with(require('ohj').fluent){

    // when(
    //     item("Debugging_Enabled").changed().toOn())
    // .then(
    //     send("Open HAB debugging is now on.").toItem("Echo_LastUsed_TTS")
    // );

    // when(
    //     item("Debugging_Enabled").changed().toOff())
    // .then(
    //     send("Open HAB debugging is now off.").toItem("Echo_LastUsed_TTS")
    // );

    when(
        item("Debugging_Enabled").changed())
    .then((it) =>
        getItem("Echo_LastUsed_TTS").sendCommand("Open HAB debugging is now " + it.newState)
    );

}

... I might be being greedy right now, but I'd rather write that like this:

    when(
        item("Debugging_Enabled").changed())
    .then(
        send("Open HAB debugging is now " + it.newState).toItem("Echo_LastUsed_TTS")
    );

... thanks again - Jamie.

jpg0 commented 4 years ago

Hmm, that exact syntax wouldn't be simple to support, because it isn't defined anywhere. It may be possible to define it as a function along with when etc, but tracking state across multiple statements may get very tricky as you need to rely on the order of execution of functions.

One option would instead to be to support templating, such that you could: send("Open HAB debugging is now ${it.newState}")...

I think that would support that direct case, but do you have others that need a similar context being passed?

JamieTemple commented 4 years ago

So I'm making good progress, but...

I can't seem to get a java.util.Timer to work:

const Timer = Java.type('java.util.Timer');
var myTimer = new Timer();

myTimer.schedule(function() { log.debug("timer completed."); }, 10000);

Throws:

TypeError : invokeMember (schedule) on JavaObject[java.util.Timer@13cb6afb (java.util.Timer)] 
failed due to: no applicable overload found (overloads

Any ideas?

jpg0 commented 4 years ago

I haven't used Timers, but you're seeing a GraalJS error which is saying that it cannot invoke the 'schedule' method on the Timer class because it cannot provide it with the correct argument types.

Looking at the method signature, it accepts a 'TimerTask'. I see that you've provided a function - this is something that GraalJS can convert to the correct type of interface for java, but only if it's an interface with a single method and is marked as a @FunctionalInterface. TimerTask is an abstract class with a few methods, so it needs explicit extension. I just tried out the following code which worked for me (note the explicit creation of a TimerTask, and the syntax to implement the 'run' method):

const Timer = Java.type('java.util.Timer');
const TimerTask = Java.type('java.util.TimerTask');
const log = require('ohj').log("logtest");

var myTimer = new Timer();

let tt = new TimerTask({
    run: function(){
        log.debug("timer completed.");
    }
});

myTimer.schedule(tt, 10000);

It's a bit cumbersome, but if you're doing it a lot it would be pretty simple to create a single JS function with callback (just like setTimeout) in a module.

JamieTemple commented 4 years ago

I haven't used Timers, but you're seeing a GraalJS error which is saying that it cannot invoke the 'schedule' method on the Timer class because it cannot provide it with the correct argument types.

Looking at the method signature, it accepts a 'TimerTask'. I see that you've provided a function - this is something that GraalJS can convert to the correct type of interface for java, but only if it's an interface with a single method and is marked as a @FunctionalInterface. TimerTask is an abstract class with a few methods, so it needs explicit extension. I just tried out the following code which worked for me (note the explicit creation of a TimerTask, and the syntax to implement the 'run' method):

const Timer = Java.type('java.util.Timer');
const TimerTask = Java.type('java.util.TimerTask');
const log = require('ohj').log("logtest");

var myTimer = new Timer();

let tt = new TimerTask({
    run: function(){
        log.debug("timer completed.");
    }
});

myTimer.schedule(tt, 10000);

It's a bit cumbersome, but if you're doing it a lot it would be pretty simple to create a single JS function with callback (just like setTimeout) in a module.

Absolute hero - works here too - thanks!

I didn't try that myself, as looking at the "old rule engine", it didn't seem necessary ... I has a feeling it might be a difference with the GraaalJS implementation - a useful lesson :)

jpg0 commented 4 years ago

When you say 'old rules engine', do you mean the Xtend-based one, or the ES5/Nashorn one?

jpg0 commented 4 years ago

Oh, and BTW, I'm thinking that if there are other things that come up, it may be better posting on the OH community pages so that others can more easily find them. If you want to tag me on the post @jpg0 so that I get notified that's fine.

JamieTemple commented 4 years ago

When you say 'old rules engine', do you mean the Xtend-based one, or the ES5/Nashorn one?

Not sure - I didn't know there was more than one.

utils.js contains:

    context.setTimeout = function(fn, millis, arg) {
        try{ 
            if( isFunction(fn) ){ //use
                var t = context.timerObject;
                if(t.timerCount > 999) t.timerCount = 0;
                var tCountLocal = t.timerCount + 1;
                t.timerCount = tCountLocal;
                t.evLoops[t.timerCount] = new Timer('jsEventLoop'+t.timerCount, false);
                t.evLoops[t.timerCount].schedule(function() {
                    fn(arg);
                    try{ 
                        //cancel and purge itself
                        if(t.evLoops[tCountLocal]){
                            t.evLoops[tCountLocal].cancel();
                            t.evLoops[tCountLocal].purge();
                        }
                    }catch(err) {
                        context.logError("utils.js setTimeout " + __LINE__ + " Error:" +  err);
                    }
                }, millis);

...
JamieTemple commented 4 years ago

Oh, and BTW, I'm thinking that if there are other things that come up, it may be better posting on the OH community pages so that others can more easily find them. If you want to tag me on the post @jpg0 so that I get notified that's fine.

No problem - somwehere like: https://community.openhab.org/c/setup-configuration-and-use/scripts-rules

???

jpg0 commented 4 years ago

That JS code you posted is from the ES5 script engine, which I agree is not 'old'! I was referring to the previous engine, which was based on the Xtend language, not JS at all. Interesting that that util.js code allows passing a function directly though; the is running on the Nashorn JS engine (which is now deprecated) - it does have some differences with the current Graal engine, mainly around interop with Java.

And yes that topic on community is fine!