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). 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).
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.
Some of the points I’m particularly curious about:
-
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?
-
The original rules used
ReentrantLock
and atry {...} 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?