Exec binding: Controlling two bluetooth Eqiva EQ3 radiator valves simultaneously

What I’m trying to achieve

One of my first projects in OpenHAB is to control two Eqiva EQ3 bluetooth-controlled radiator valves in my living room.

Platform information

  • Hardware: Raspberry Pi 3B+
  • OS: OpenHABian
  • Java Runtime Environment: openjdk version “1.8.0_265”
  • openHAB version: 2.5.9-1

What I’ve done so far

Starting point

I found a detailed guide on how to use the exec binding in the forum. I adapted that guide to my application and then changed the rule code to follow the recommended 1-2-3 design pattern.

That configuration sometimes worked, but at other times, it didn’t. So I went back to the forums, and found out, that I shouldn’t (probably ever) use thread::sleep or RentrantLock at all. So I changed my setup once again in reference to the exec binding’s documentation full example.

Implementation

Bash script

One of my two valves is at the other end of the room from the Raspberry Pi. This sometimes results in an unsuccessful connection for that valve. At that point it is usually enough to retry the command a few times. After having been unsuccessful with automating that in the rule itself (1. call the script in a seperate rule, which is invoked by a proxy item that is changed in the start rule 2. check the exec:output channel and, if the exec binding’s output channel hints at a connection problem, invoke the calling rule again), I wrote a small wrapper around that, which now instead fires the eq3 control script again in case of a connection (or any other) error. I called this script control_eq3.sh and created an entry for it in the whitelist.

#!/bin/bash

n=0
until [ "$n" -ge 5 ]
do
   eq3cli "$@" && break     # '"$@"' means "all passed arguments"
                            # break if return value of 'eq3cli "$@"' is '0'
   n=$((n+1)) 
   sleep 5
done

Thing

Thing exec:command:eq3cli [
            command="/etc/openhab2/scripts/control_eq3.sh %2$s",
            interval=0,
            autorun=false]

Items

// items to show in sitemap
Group:Switch:OR(OFF, ON) eq3_control <radiator> // group to be triggered (icon: radiator)
Switch eq3_wozi_sofa <radiator> (eq3_control) // member of group eq3_control
Switch eq3_wozi_balkon <radiator> (eq3_control)
String eq3_wozi_balkon_temp <temperature>
String eq3_wozi_sofa_temp <temperature>
String PlaceHolder ""

//items to send command
Switch eq3cli_isRunning  { channel="exec:command:eq3cli:run", autoupdate='false' } // is script running?
String eq3cli_args    { channel="exec:command:eq3cli:input"  } // input arguments to script
String eq3cli_return  { channel="exec:command:eq3cli:output" } // return value of script

Sitemap

sitemap heizung label="Heizung"
{
    Frame label="Wohnzimmer"
    {
        Switch item=eq3_control mappings=[ON="An",OFF="Aus"] label="Wohnzimmer"
        Text item=PlaceHolder label="" icon="none"
        Switch item=eq3_wozi_sofa mappings=[ON="An",OFF="Aus"] label="Sofa"
        Switch item=eq3_wozi_balkon mappings=[ON="An",OFF="Aus"]  label="Balkon"
    }
}

Rules

val MAC_eq3_wozi_sofa = "00:1A:22:10:68:FE"
val MAC_eq3_wozi_balkon = "00:1A:22:10:3D:28"

rule "eq3: set parameters"
    when
        Member of eq3_control received command
    then
        // 1. Check to see if the Rule has to run at all, if not exit.
        logInfo("eq3: set parameters", "Member " + triggeringItem.name + " changed to " + receivedCommand)

        // 2. Calculate what needs to be done.
        val radiator = triggeringItem.name.toString
        var MAC_radiator = MAC_eq3_wozi_balkon
        if (radiator == "eq3_wozi_sofa")
        {
            MAC_radiator = MAC_eq3_wozi_sofa
        }

        var targetTemp = 18 // default: setting eco mode
        if (receivedCommand == ON)
        {
            targetTemp = 23
        }

        // 3. Do it. Only do actions like sendCommand in one place at the end of the Rule.
        logInfo("eq3: set parameters", "radiator: " + radiator + "; targetTemp: " + targetTemp + "; MAC_radiator: " + MAC_radiator) 
        eq3cli_args.sendCommand("--mac " + MAC_radiator + " temp --target " + targetTemp)
end

rule "eq3: execute command"
when
    Item eq3cli_args received command
then
        // Separately triggering RUN allows subsequent executions with unchanged parameter %2
        // which autorun does not.
    if (eq3cli_isRunning.state != ON) 
    {
        logInfo("eq3: execute command", "Run script with these parameters: " + eq3cli_args.state.toString)
        eq3cli_isRunning.sendCommand(ON)
    }

    else
    {
        logInfo("eq3: execute command", "Script already in use, skipping execution.")
    }
end

rule "eq3cli_isRunning changed"
    when
        Item eq3cli_isRunning changed
    then
        logInfo("eq3cli_isRunning changed", "eq3cli_isRunning changed to " + triggeringItem.state.toString)
end

rule "eq3cli_return changed"
when
    Item eq3cli_return received update
then
        // Logging of raw command line result and write returned temperature in Sitemap
        val scriptOutput = triggeringItem.state.toString
        val returnedTemperature = transform("REGEX",".*Setting.+\\: (\\d\\d.\\d).*", scriptOutput)
        logInfo("eq3cli_return changed", "eq3cli_return changed to " + scriptOutput.replaceAll("\r|\n"," ") )
        logInfo("eq3cli_return changed", "Returned temperature: " + returnedTemperature)
end

Behavior with respective log entries

A. Turn the valve “eq3_wozi_balkon” ON (after having turned on the other valve “eq3_wozi_sofa” in the previous step)

2020-12-09 12:11:38.387 [INFO ] [ome.model.script.eq3: set parameters] - Member eq3_wozi_balkon changed to ON
2020-12-09 12:11:38.416 [INFO ] [ome.model.script.eq3: set parameters] - radiator: eq3_wozi_balkon; targetTemp: 23; MAC_radiator: 00:1A:22:10:3D:28
2020-12-09 12:11:38.452 [INFO ] [me.model.script.eq3: execute command] - Run script with these parameters: --mac 00:1A:22:10:68:FE temp --target 23
2020-12-09 12:11:38.572 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Passing to shell for parsing command.
2020-12-09 12:11:38.575 [DEBUG] [ng.exec.internal.handler.ExecHandler] - OS: *NIX (Linux)
2020-12-09 12:11:38.577 [TRACE] [ng.exec.internal.handler.ExecHandler] - The command to be executed will be '[sh, -c, /etc/openhab2/scripts/control_eq3.sh --mac 00:1A:22:10:3D:28 temp --target 23]'
2020-12-09 12:11:38.588 [INFO ] [odel.script.eq3cli_isRunning changed] - eq3cli_isRunning changed to ON

==> /var/log/openhab2/events.log <==
2020-12-09 12:11:38.358 [ome.event.ItemCommandEvent] - Item 'eq3_wozi_balkon' received command ON
2020-12-09 12:11:38.376 [vent.ItemStateChangedEvent] - eq3_wozi_balkon changed from OFF to ON
2020-12-09 12:11:38.381 [GroupItemStateChangedEvent] - eq3_control changed from OFF to ON through eq3_wozi_balkon
2020-12-09 12:11:38.431 [ome.event.ItemCommandEvent] - Item 'eq3cli_args' received command --mac 00:1A:22:10:3D:28 temp --target 23
2020-12-09 12:11:38.462 [nt.ItemStatePredictedEvent] - eq3cli_args predicted to become --mac 00:1A:22:10:3D:28 temp --target 23
2020-12-09 12:11:38.481 [ome.event.ItemCommandEvent] - Item 'eq3cli_isRunning' received command ON
2020-12-09 12:11:38.539 [vent.ItemStateChangedEvent] - eq3cli_args changed from --mac 00:1A:22:10:68:FE temp --target 23 to --mac 00:1A:22:10:3D:28 temp --target 23
2020-12-09 12:11:38.587 [vent.ItemStateChangedEvent] - eq3cli_isRunning changed from OFF to ON

==> /var/log/openhab2/openhab.log <==
2020-12-09 12:11:43.016 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [OUTPUT]: 'Current target temp: 22.0'
2020-12-09 12:11:43.019 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [OUTPUT]: 'Setting target temp: 23.0'
2020-12-09 12:11:43.395 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Transformed response is 'Current target temp: 22.0
Setting target temp: 23.0'
2020-12-09 12:11:43.410 [INFO ] [odel.script.eq3cli_isRunning changed] - eq3cli_isRunning changed to OFF
2020-12-09 12:11:43.426 [INFO ] [e.model.script.eq3cli_return changed] - eq3cli_return changed to Current target temp: 22.0 Setting target temp: 23.0
2020-12-09 12:11:43.434 [INFO ] [e.model.script.eq3cli_return changed] - Returned temperature: 23.0

==> /var/log/openhab2/events.log <==
2020-12-09 12:11:43.402 [vent.ItemStateChangedEvent] - eq3cli_isRunning changed from ON to OFF

After this, valve “eq3_wozi_balkon” tuns to 23.0 °C (as expected).

B. Switch valve group “eq3_control” to ON

2020-12-09 13:00:50.231 [INFO ] [ome.model.script.eq3: set parameters] - Member eq3_wozi_sofa changed to OFF
2020-12-09 13:00:50.248 [INFO ] [ome.model.script.eq3: set parameters] - Member eq3_wozi_balkon changed to OFF
2020-12-09 13:00:50.262 [INFO ] [ome.model.script.eq3: set parameters] - radiator: eq3_wozi_balkon; targetTemp: 18; MAC_radiator: 00:1A:22:10:3D:28
2020-12-09 13:00:50.262 [INFO ] [ome.model.script.eq3: set parameters] - radiator: eq3_wozi_sofa; targetTemp: 18; MAC_radiator: 00:1A:22:10:68:FE
2020-12-09 13:00:50.281 [INFO ] [me.model.script.eq3: execute command] - Run script with these parameters: --mac 00:1A:22:10:3D:28 temp --target 23
2020-12-09 13:00:50.303 [INFO ] [me.model.script.eq3: execute command] - Run script with these parameters: --mac 00:1A:22:10:3D:28 temp --target 23
2020-12-09 13:00:50.397 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Passing to shell for parsing command.
2020-12-09 13:00:50.399 [DEBUG] [ng.exec.internal.handler.ExecHandler] - OS: *NIX (Linux)
2020-12-09 13:00:50.402 [TRACE] [ng.exec.internal.handler.ExecHandler] - The command to be executed will be '[sh, -c, /etc/openhab2/scripts/control_eq3.sh --mac 00:1A:22:10:68:FE temp --target 18]'
2020-12-09 13:00:50.410 [INFO ] [odel.script.eq3cli_isRunning changed] - eq3cli_isRunning changed to ON
2020-12-09 13:00:50.450 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Passing to shell for parsing command.
2020-12-09 13:00:50.452 [DEBUG] [ng.exec.internal.handler.ExecHandler] - OS: *NIX (Linux)
2020-12-09 13:00:50.457 [TRACE] [ng.exec.internal.handler.ExecHandler] - The command to be executed will be '[sh, -c, /etc/openhab2/scripts/control_eq3.sh --mac 00:1A:22:10:68:FE temp --target 18]'

==> /var/log/openhab2/events.log <==
2020-12-09 13:00:50.211 [ome.event.ItemCommandEvent] - Item 'eq3_control' received command OFF
2020-12-09 13:00:50.219 [ome.event.ItemCommandEvent] - Item 'eq3_wozi_sofa' received command OFF
2020-12-09 13:00:50.225 [ome.event.ItemCommandEvent] - Item 'eq3_wozi_balkon' received command OFF
2020-12-09 13:00:50.242 [vent.ItemStateChangedEvent] - eq3_wozi_sofa changed from ON to OFF
2020-12-09 13:00:50.248 [vent.ItemStateChangedEvent] - eq3_wozi_balkon changed from ON to OFF
2020-12-09 13:00:50.270 [ome.event.ItemCommandEvent] - Item 'eq3cli_args' received command --mac 00:1A:22:10:3D:28 temp --target 18
2020-12-09 13:00:50.292 [ome.event.ItemCommandEvent] - Item 'eq3cli_args' received command --mac 00:1A:22:10:68:FE temp --target 18
2020-12-09 13:00:50.312 [nt.ItemStatePredictedEvent] - eq3cli_args predicted to become --mac 00:1A:22:10:3D:28 temp --target 18
2020-12-09 13:00:50.328 [ome.event.ItemCommandEvent] - Item 'eq3cli_isRunning' received command ON
2020-12-09 13:00:50.345 [nt.ItemStatePredictedEvent] - eq3cli_args predicted to become --mac 00:1A:22:10:68:FE temp --target 18
2020-12-09 13:00:50.360 [ome.event.ItemCommandEvent] - Item 'eq3cli_isRunning' received command ON
2020-12-09 13:00:50.377 [vent.ItemStateChangedEvent] - eq3cli_args changed from --mac 00:1A:22:10:3D:28 temp --target 23 to --mac 00:1A:22:10:3D:28 temp --target 18
2020-12-09 13:00:50.380 [vent.ItemStateChangedEvent] - eq3cli_args changed from --mac 00:1A:22:10:3D:28 temp --target 18 to --mac 00:1A:22:10:68:FE temp --target 18
2020-12-09 13:00:50.407 [vent.ItemStateChangedEvent] - eq3cli_isRunning changed from OFF to ON

==> /var/log/openhab2/openhab.log <==
2020-12-09 13:00:56.490 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [OUTPUT]: 'Current target temp: 23.0'
2020-12-09 13:00:56.493 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [OUTPUT]: 'Setting target temp: 18.0'
2020-12-09 13:00:56.799 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Transformed response is 'Current target temp: 23.0
Setting target temp: 18.0'
2020-12-09 13:00:56.807 [INFO ] [odel.script.eq3cli_isRunning changed] - eq3cli_isRunning changed to OFF
2020-12-09 13:00:56.822 [INFO ] [e.model.script.eq3cli_return changed] - eq3cli_return changed to Current target temp: 23.0 Setting target temp: 18.0
2020-12-09 13:00:56.826 [INFO ] [e.model.script.eq3cli_return changed] - Returned temperature: 18.0

==> /var/log/openhab2/events.log <==
2020-12-09 13:00:56.803 [vent.ItemStateChangedEvent] - eq3cli_isRunning changed from ON to OFF
2020-12-09 13:00:56.813 [vent.ItemStateChangedEvent] - eq3cli_return changed from Current target temp: 23.0
Setting target temp: 23.0 to Current target temp: 23.0
Setting target temp: 18.0
2020-12-09 13:00:58.196 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [OUTPUT]: 'Current target temp: 18.0'
2020-12-09 13:00:58.199 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [OUTPUT]: 'Setting target temp: 18.0'
2020-12-09 13:00:58.547 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [ERROR]: 'Traceback (most recent call last):'
2020-12-09 13:00:58.549 [DEBUG] [ng.exec.internal.handler.ExecHandler] - Exec [ERROR]: '  File "/usr/local/lib/python3.7/dist-packages/eq3bt/connection.py", line 36, in __enter__'
// continued error message from shell
bluepy.btle.BTLEDisconnectError: Failed to connect to peripheral 00:1A:22:10:68:FE, addr type: public
// continued eror message from shell
2020-12-09 13:00:58.678 [INFO ] [e.model.script.eq3cli_return changed] - eq3cli_return changed to Current target temp: 18.0 Setting target temp: 18.0 Traceback (most recent call last):   File "/usr/local/lib/python3.7/dist-packages/eq3bt/connection.py", line 36, in __enter__ // ... (continues as above)
2020-12-09 13:00:58.683 [INFO ] [e.model.script.eq3cli_return changed] - Returned temperature: 18.0
2020-12-09 13:00:58.658 [vent.ItemStateChangedEvent] - eq3cli_return changed from Current target temp: 23.0
Setting target temp: 18.0 to Current target temp: 18.0
Setting target temp: 18.0
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/dist-packages/eq3bt/connection.py", line 36, in __enter__ 
// ... (continues as above)
bluepy.btle.BTLEDisconnectError: Failed to connect to peripheral 00:1A:22:10:68:FE, addr type: public

After this, only valve “eq3_wozi_sofa” is set to the target temperature.

My issues and questions

As I stated above, the control of the valves itself is mostly working (if only one valve is controlled at a time). :slight_smile: However, there are a few issues that prevent me from adapting it further to my needs (like setting both valves to on based on a calendar entry).

I wasn’t sure if it is wise to post them all in the same thread, but I figured, it makes even less sense to duplicate of all of the above in different threads with only the question being different.

1. Order of execution

My first question concerns the first behavior (A, see above). I’m trying to write a message about which parameters are passed to the script using the statement

logInfo("eq3: execute command", "Run script with these parameters: " + eq3cli_args.state.toString)`

As I’m doing this only after the exec:input channel receives its command (i.e. in the rule eq3: execute command), I would expect, that this would output the arguments to be passed. However, it turns out, that the thing eq3cli_args is only updated some milliseconds after the logInfo statement and so what logInfo gives me, are actually the arguments from the last call when I controlled the other valve.

It appears to me that I don’t fully understand those two log lines’ implications yet:

2020-12-09 12:11:38.431 [ome.event.ItemCommandEvent] - Item 'eq3cli_args' received command --mac 00:1A:22:10:3D:28 temp --target 23
2020-12-09 12:11:38.462 [nt.ItemStatePredictedEvent] - eq3cli_args predicted to become --mac 00:1A:22:10:3D:28 temp --target 23

Can anyone explain to me what’s going on here? How can I make sure to output the correct arguments?

2. Setting both valves via the group item

This question refers to the second behavior (B, see above). The errors and behavior apparently arise, because the Raspberry Pi can only make one bluetooth connection at a time. That means, that both valves can’t be controlled at the very same time physically.

Now, I want to make sure of that in my setup. The statements

if (eq3cli_isRunning.state != ON) 
    {
        // ...
    }

    else
    {
        logInfo("eq3: execute command", "Script already in use, skipping execution.")
    }

prevent the execution of one valve if the other one was set by me maybe half a second before (and so the script is still running). As a result, the script isn’t executed for the second valve and I find the message

[INFO ] [me.model.script.eq3: execute command] - Script already in use, skipping execution.

in the logs (as expected). However, this doesn’t work if both valves are set at nearly the same time by the group switch.

How can I make sure, that in that case (and also for setting both valves manually shortly after each other), one valve gets set and only after that has happened and eq3cli_isRunning changes to OFF again, the script is excuted for the other valve?

One of the threads that I linked above makes use of ReentrantLock (which I don’t fully understand yet) and thread::sleep in a while loop in the following manner. Note that the thing was configured to autorun=true in that example.

import java.util.concurrent.locks.ReentrantLock
val ReentrantLock eq3_script = new ReentrantLock

rule "Control eq3"
    when
        Member of eq3_control received command
    then
        // ...
        eq3cli_args.sendCommand(...)
        while(eq3cli_isRunning.state != OFF)
        {
            Thread::sleep(100)
        }
        Thread::sleep(400)
        eq3_script.unlock()
end

However, that didn’t work reliably and also uses thread::sleep in a loop and also uses ReentrantLock. As I read in the linked DP, that both of those shouldn’t be used, I’m now looking for a reliably working alternative (i.e. if two valves are turned on at the same time or shorty after each other, they will still both change their temperature, but one after another). :slight_smile:

3. Showing the set temperature on the sitemap

When issue 1 and 2 are solved, I would want to show the set temperature in the sitemap. My idea is to write the return value of

transform("REGEX",".*Setting.+\\: (\\d\\d.\\d).*", eq3cli_return.state.toString)

(see last rule) to one item for each valve that I can then show in the sitemap. However, assuming that I change both valves’ temperatures at once, how will I know if eq3cli_return refers to valve “eq3_wozi_balkon” or “eq3_wozi_sofa”?

It could be a workaround to output the MAC in my shell script and transform that in the rule to identify the valve. However, that seems like making things unnecessarily complicated when I believe that this can be handled in the rule itself.

Perhaps the solution could be a proxy item which carries the triggering valve item? However, for that to work, issue 2 would have to be solved first.

4. Improvements

Now, after having seen the code I came up with, where you see room for improvement? I’ve already learnt a lot from reading these forum, especially the awesome design pattern posts (they should really go to the docs!), and I’d like to learn more. :slight_smile:

Some of the points I’m particularly curious about:

  1. I’m now putting labels in my .sitemap file and icons in the .items (or even a mix), but I’ve also seen this in other ways. Is there a common recommendation about what to put where?

  2. The original rules used ReentrantLock and a try {...} catch(throwable t) {} finally {eq3_script.unlock} concept. I’m not quite clear why, though. Should I use any of those and if yes, why? If not, why not?

Note for you and future readers. That advice most definitely applies to OH 2.5. However, it does not apply to OH 3.

In OH 3 there is no need for reentrant locks because only one instance of a given rule can run at a time. So that’s already done for you be default. And there is no longer a thread pool that rules run out of. Reach rule gets it’s own thread so long running rules will not starve out other rules. But it will cause additional triggers of that rule to be queued up.

For that reason it’s still a good idea to avoid long running rules, but it’s not as important as it once was.

it takes some time, couple hundred msec or so, for an item to actually change it’s state in response to a postUpdate. It takes even longer (and sometimes never) for an item to change state in response to a sendCommand. By calling .state immediately after sendCommand or postUpdate, you will get the previous Item’s state since it hasn’t had a chance to change yet. This all happens asynchronously.

The items are configured with autoupdate=true. This is configured by default. When you send a command to such an item, the autoupdate mechanism predicts what state the item should become in response to the command. Remember that but all commands are states (e.g. INCREASE). If the binding doesn’t override autoupdate, the item will become updated to that predicted state independent of start the actually device does.

There is a design pattern called Gatekeeper for just this use case. It involves proxy items that receive the commands that triggers a rule that prevents them from being issued to the items linked to the devices too fast. Design Pattern: Gate Keeper - #17 by anfaenger

Another approach would be to use a proxy item instead of a Group. In a rule send a command to one and then schedule a timer to send command to the other one after a suitable amount of time.

In GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules. I have a number of utilities that can be used with Jython and I’m starting to add OH 3 JavaScript versions that implement most of this stuff for you.

No, it will be easiest to have your script output something that identifies which valve it controlled.

I’m on my phone so can’t do a proper analysis. If I remember I’ll come back and have a go at a review.

What ever makes sense for you. I tend to define everything at the item but override that at the sitemap sometimes where necessary.

Because of the rule triggers again while it’s already running you will have two copies of the rule running at the same time. The reentrant lock prevents the second one from continuing until the first one released the lock. It’s dangerous because the instance that us waiting for the lock consumes a rule thread from the pool. If you have more than 5 such rule instances waiting around no other errors can run. As I said above, the behavior in OH 3 is different. Only one instance if the rule can run at a time so there is no need for the reentrant lock.
The original code needed the lock to prevent that second insurance if the rule for sending it’s command too soon after the first by blocking it from running until a certain amount of time after the first instance sent it’s command.

1 Like

I’m now wondering if I should just wait for OH 3 and issue #2 will be solved automatically?!

Is there any chance for me to know when the item’s state actually changed, so that I can do logInfo only then? Just waiting for maybe 100 milliseconds seems quite unsatisfactory for me because that seems like a somewhat arbitrary number and actualy, it might sometimes be faster, sometimes be slower. Especially since everything is “asynchronous” (which gives me a total brain fuck by the way - not used to that at all).

Thank you for pointing me to the DP! I’ve spent a few hours trying to understand it, but I’m not quite there yet.

  1. How do I know how I long in the future should I should reschedule my timer?

    1. Do I just assume some maximum time, that my command might run (say 30 seconds) and set that as the time? I could live with that (after all, it’s not that time-critical), but I’m not that satisfied with the solution because again, I’m not sure about the time and it might as well just finish after 3 seconds. Then I’d have “wasted” 27 seconds.

    2. Can I somehow halt the execution of the rule until the script is not running anymore without violating the “no long-running rules” advice? Could I use a timer to check if it is still running every second or so? Or would I even have to extend the DP’s example to if(commands.peek !== null && isRunning == false)?

  2. The example uses executeCommandLine. I guess, the script is given time until the specified time-out and only then the next commands are excuted. Is that correct?

  3. I don’t understand in which rule instance the timer will be executed. Or perhaps that doesn’t matter since the timer is a global variable anyway, so the timer will just be set and rescheduled in the rule instance that was run first? All the second rule instance does then, is adding another command to the queue which the (global) timer in the first rule instance will then work on once it reaches null again. Is that correct?

  4. So I guess, a good place to put the timer would be the very first rule (eq3: set parameters) and put all of its existing code inside the timer. Is that correct?

  5. What is the difference between timer === null and timer == null?

Hmm, that might indeed be simpler than I thought since the MACs are global variables anyway. That means I could output the MAC by my shell script, parse that in the rule using regex, and identify the valve with the global MAC variable. Would I then use a condition to check which MAC and then set either the text item for valve 1 or the one of valve 2 to the returned temperature? Or is there a cleaner way? I saw somewhere that I can identify an item by string manipulation. Perhaps, I should use that.

As an alternative, could I use executeCommandLine and write the return value into a proxy item immediately? Then I could, based on the valve to be controlled, write that proxy item’s state into either the sitemap item for valve 1 or valve 2.

I would appreciate that. :slight_smile:

You don’t really have to wait. There’s nothing magic about the release really. Especially the first release.

No, but why wait? You already know what you’ve updated the Item to so log/use that instead of trying to get the state you already know you sent to the Item from the Item. The only real thing you can do is split the rule up. Send the update or command in one Rule and then have another rule trigger when the Item changes state to continue on executing the task.

You’ll have to experiment to see how quickly you can send commands to the device and always have the command received.

No. But you could create a Timer to schedule code to run later after the Rule exits.

That is certainly an option as well. However, remember that it’s asynchronous so if you send the commands too close together, isRunning may still be false when the second command is received and you’ll still be stuck with both running at the same time.

The easiest thing to do would be to experimentally figure out the optimum time and use that in the Gatekeeper to separate the commands by that optimum amount of time.

A harder approach would be to modify the Gatekeeper to watch the running Channel of all the Things and queue up the commands as long as that shows one of the Things is running the command. You’ll have to be very careful to avoid that brief period between issuing the command and the running state updating.

When you pass a timeout parameter to executeCommandLine it waits up to that number of milliseconds before giving up on the command and returning with an error. While the script is executing the rule is blocked from continuing so the script either needs to complete or time out for the rest of the rule to execute. However, this results in long running rules so this can cause problems just like Thread::sleeps can.

In the Gatekeeper design pattern, that is addressed somewhat by moving that long running executeCommandLine (and the sleeps) to a Timer. But that is really just moving the root problem from one thread pool to another so you still have to be careful.

What that specific example in the Gatekeeper DP does is allows up to five seconds for the call to executeCommandLine to execute. Any amount of time it took to run the executeCommandLine is subtracted from the 100 msec buffer and the timer is rescheduled to process the next command from there.

It is perfectly reasonable and expected that the DP would be extended to other use cases, such as basing the loop on the states of the running Items for Exec binding Items. But you’ll still need some locks and timers to deal with the asynchronous updates to those Items.

Timers, or more properly stated the lambdas that are passed to the Timers, inherit the context that existed at the time it was created. All the variables that exist and are accessible when that [ | // timer code ] occurs will be accessible when that lambda is eventually called by the Timer.

I’m not sure how to address the second half. The Gatekeeper is just the one rule. In the examples I show a second rule that shows how to use the Gatekeeper by sending a command to the proxy Item that drives the Gatekeeper rule itself.

The flow is:

  1. The proxy Item Outlet_A receives a command
  2. The “Outlet A” rule triggers
  3. Based on the command a new command is sent to Wireless_Controller. This Item is the proxy Item that drives the Gatekeeper.
  4. The Gatekeeper Rule runs
  5. The command is added to the queue of commands.
    6.a. If the Gatekeeper timer is already running the rule is done and exits. The command will be worked off the queue in time as the timer processes the queue.
    6.b. If the timer is not already running, create the timer. The timer looks to see if there is a command. If so it executes the command and then reschedules the Timer to run again in 100 msecs (minus any overhead time taken up by book keeping in the timer) where the next command it run. This repeats until the queue empties.

I’d need to analyze the rule much more closely. But I don’t want to spend too much time on that if you’re going to take the easier way and just move to OH 3 where most of these problems go away.

== is equivalency
=== is identity

The statement foo == bar is the same as calling foo.equals(bar).

The statement foo === bar is the same as saying "does foo point to the same place in memory as bar?`

null isn’t an Object and therefore it doesn’t have a .equals method and null is a constant so there is only one null in memory that everything in openHAB is pointing to when using the word null. So when we want to compare something to see if it’s null, we want to use the identity comparison instead of the equivalence comparison. You’ll get warnings in the log if you don’t.

Depends on what you mean by “immediately.” Like has been discussed, updating Items is an asynchronous action. If you postUpdate to the Item, it will be some number of milliseconds before MyItem.state will reflect that new state. But in your rule you’ll already have that new state so you can just use that instead of trying to pull it back from the Item.