I’m back at it. After stepping back to centralize the handling of multiple timers I’ve moved back to migrating individual Rules.
Alarms
I have three ZCombo Smoke/CO alarms, one on each floor of the house. If any one of them go off, I go into a loop and generate an alert every five seconds and flash the lights. NOTE: The Rules DSL version never quite worked right but the new JSR223 version works like a champ.
Rules DSL
val logName = "alarms"
rule "An alarm is going off!"
when
Member of gAlarms changed
then
if(previousState == NULL) return;
logInfo(logName, transform("MAP", "admin.map", triggeringItem.name) + " changed state to " + triggeringItem.state)
if(gAlarms.state == ON) aAlarmTimer.sendCommand(ON)
else aAlarmTimer.postUpdate(OFF)
end
rule "Flash the light and send alerts!"
when
Item aAlarmTimer received command
then
val onAlarms = gAlarms.members.filter[ alarm | alarm.state == ON ]
if(onAlarms.size == 0) {
aAlert.sendCommand("The alarms have stopped.")
aAlarmTimer.postUpdate(OFF)
return;
}
if(receivedCommand == ON) {
gLights_ALL.sendCommand(if(gLights_ALL.state == ON) OFF else ON)
val alertMsg = new StringBuilder
alertMsg.append("There are " + onAlarms.size + " smoke/CO alarms going off: ")
onAlarms.forEach[ SwitchItem alarm | alertMsg.append(transform("MAP", "admin.map", alarm.name) + ", ") ]
alertMsg.delete(alertMsg.length - 2, alertMsg.length)
aAlert.sendCommand(alertMsg.toString)
}
else {
// We already found that there are some alarms going off so reschedule the timer
aAlarmTimer.sendCommand(ON)
}
end
The looping timer is implemented using the Expire binding. When an alarm goes off, start a looping Timer that generates an alert showing all the currently going off alarms in the message and toggle the lights. Stop the loop when all the alarms stop.
Python
from core.rules import rule
from core.triggers import when
from personal.util import get_name, send_alert, toggle_switch
from core.actions import ScriptExecution
from org.joda.time import DateTime
from core.log import log_traceback
alarm_timer = None
@log_traceback
def num_alarms(): return len(filter(lambda alarm: alarm.state == ON, ir.getItem("gAlarms").members))
@log_traceback
def alarm_active(log):
"""Sets up a looping timer that continues as long as at least one alarm is ON"""
global alarm_timer
on_alarms = filter(lambda alarm: alarm.state == ON, ir.getItem("gAlarms").members)
num = len(on_alarms)
# IF num is 0 all the alarms have stopped
if num == 0:
send_alert("The alarms have stopped!", log)
# log.info("Alarms have stopped!")
alarm_timer = None
# Send an alert
else:
mapped = map(lambda alarm: "{}".format(get_name(alarm.name)), on_alarms)
msg = "There {} {} Smoke/CO {} going off: {}".format("is" if num == 1 else "are", num, "alarm" if num == 1 else "alarms", ", ".join(mapped))
send_alert(msg, log)
# log.info(msg)
toggle_switch("gLights_ALL")
alarm_timer = ScriptExecution.createTimer(DateTime.now().plusSeconds(5), lambda: alarm_active(log))
@rule("Alarm!", description="An alarm is going off!", tags=["alarm"])
@when("Member of gAlarms changed")
def alarm(event):
if isinstance(event.oldItemState, UnDefType): return
global alarm_timer
if event.itemState == ON: alarm.log.info("{} is going off!".format(get_name(event.itemName)))
if num_alarms() == 1 and (alarm_timer is None or alarm_timer.hasTerminated()): alarm_active(alarm.log)
@rule("Alarms startup", description="Start the looping timer if the alarms are ON when OH starts", tags=["alarm"])
@when("System started")
def alarm_start(event):
if num_alarms() > 0:
alarm_start.log.info("There are alarms going off when OH restarted!")
alarm_active(alarm_start.log)
This version does the same thing. I think that I have another Rule that toggles the state of a Switch so I moved that logic to the library.
from core.jsr223.scope import ir, events
from core.log import log_traceback
from org.eclipse.smarthome.core.library.types import OnOffType
@log_traceback
def toggle_switch(itemName):
item = ir.getItem(itemName)
events.sendCommand(itemName, "ON" if str(item.getStateAs(OnOffType)) == "OFF" else "OFF")
I’m finding that Expire binding doesn’t buy you as much in terms of simplification with Jython Rules compared to Rules DSL so I’ve been using regular Timers instead. I added a Rule to start the alerting if there is an alarm going off when OH first starts. As in the previous Rules, I’ve added a human friendly name to the Items as metadata.
Send the Master Bedroom Roku to the home screen at Bed time
My wife likes to go to sleep with the TV on (I hate it but what can you do). We are “cord cutters” and my ISP has a 1 TB per month usage cap. So I don’t want to have the Roku sitting there streaming all night. For a time, Netflix and Hulu would not stop streaming on their own so I wrote this Rule just to nudge it to the home screen, stopping the streaming at bed time.
Rules DSL
rule "Send MBR Roku to home at night"
when
Item vTimeOfDay changed to "BED" or
Item aSendMbrRoku_Home received command
then
logInfo("media", "Sending the Roku to bed")
aMbrRoku_Home.sendCommand(ON)
createTimer(now.plusSeconds(5), [|
aMbrRoku_Home.sendCommand(ON)
logInfo("media", "Sent the MBR Roku to the home screen: " + vMbrRoku_Home_Result.state.toString)
])
end
aMbrRoku_Home is bound to MQTT which sends a message to an external script that issues the actual command to the Roku. I had networking issues at one point and I couldn’t get it to work from OH itself. Now I could probably use the HTTP binding to do this.
I need to issue the command twice because if it has stopped playing for some reason, the first command just wakes the screen and the second one actually sends it to home.
Python
from core.rules import rule
from core.triggers import when
from core.actions import ScriptExecution
from org.joda.time import DateTime
def send_home(log):
log.info("Sending the MBR Roku to home")
events.sendCommand("aMbrRoku_Home", "ON")
@rule("MBR Roku Home", description="Sends the master bedroom's Roku to the home screen", tags=["media"])
@when("Item vTimeOfDay changed to 'BED'")
@when("Item aSendMbrRoku_Home received command")
def roku_home(event):
send_home(roku_home.log)
ScriptExecution.createTimer(DateTime.now().plusSeconds(5), lambda: send_home(roku_home.log))
Notice how in the past two Rules I am making use of functions where appropriate.
Update the Nest Home/Away based on OH’s Presence
I did not like how the Nest tracked presence so I use OH instead.
Rules DSL
rule "vPresent changed, update Nest"
when
Item vPresent changed
then
aNest_Away.sendCommand(if(vPresent.state == ON) "HOME" else "AWAY")
end
Python
from core.rules import rule
from core.triggers import when
@rule("Update Nest Away", description="Synchronizes Nest Away with vPresent", tags=["presence", "nest"])
@when("Item vPresent changed")
def update_nest_away(event):
events.sendCommand("aNest_Away", "HOME" if event.itemState == ON else "AWAY")
When I replace my Nest, this will probably no longer be necessary.
Reset the Presence at start
I have my presence Rules configured to assume that no one is home on OH restart until someone is proven to be home. This Rule enforces that.
Rules DSL
rule "Reset vPresent to OFF on startup"
when
System started
then
vPresent.sendCommand(OFF)
gPresent.sendCommand(OFF)
end
Python
from core.rules import rule
from core.triggers import when
@rule("Initialize present to OFF", description="Updates all the present Items to OFF", tags=["presence"])
@when("System started")
def reset_presence(event):
events.sendCommand("vPresent", "OFF")
events.sendCommand("gPresent", "OFF") # will be forwarded to all members
Presence Detection
This is the inspiration for Generic Presence Detection.
rule "A presence sensor updated"
when
Item gPresent changed
then
logInfo(logName, "gPresent changed to " + gPresent.state)
if(tPresent.state == ON && gPresent.state == vPresent.state) {
logInfo(logName, "Timer is running but group and proxy are the same, cancelling timer")
tPresent.postUpdate(OFF)
}
else if(gPresent.state == vPresent.state) return;
if(gPresent.state == OFF) {
logInfo(logName, "Everyone is away, setting anti-flapping timer")
tPresent.sendCommand(ON)
}
else if(gPresent.state == ON) {
logInfo(logName, "Someone came home, setting presence to ON")
vPresent.sendCommand(ON)
}
end
Python
from core.rules import rule
from core.triggers import when
from core.actions import ScriptExecution
from org.joda.time import DateTime
from core.log import log_traceback
presence_timer = None
@log_traceback
def all_away(log, events):
try:
global presence_timer
events.sendCommand("vPresent", "OFF")
presence_timer = None
except:
print "Error in all_away:", sys.exec_info()[0]
raise
@rule("Presence", description="Update vPresence based on the states of gPresent's members", tags=["presence"])
@when("Item gPresent changed")
def presence(event):
global presence_timer
# If the group and proxy are the same, cancel the timer if it exists and return
if items["gPresent"] == items["vPresent"]:
if presence_timer is not None and not presence_timer.hasTerminated():
presence.log.info("Someone came home, cancelling presence antiflapping timer")
presence_timer.cancel()
presence_timer = None
return
# group state differs from the proxy state, create a Timer if it went OFF. The condition above handles the
# case where it went ON again while the Timer is running
if event.itemState == OFF:
presence.log.info("Everyone is away, setting the anti-flapping timer")
presence_timer = ScriptExecution.createTimer(DateTime.now().plusMinutes(2), lambda: all_away(presence.log, events))
elif event.itemState == ON:
presence.log.info("Someone came home, setting presence to ON")
events.sendCommand("vPresent", "ON")
I had some problems with the handling of my presence_timer variable which is why there is the try/except in the timer function. I needed to promote the variable to global in order to reference it inside the Rule and the timer body. Otherwise I would get a “trying to reference presence_timer before it is assigned” or something like that.
Calculate Estimated Power Bill
There are two Rules here, one to reset the running total on the last day of the billing cycle and another to keep a running total of the estimated bill.
Rules DSL
val rate = 0.1066
val gridAccess = 34.0
rule "Reset meter on first Monday of the month"
when
Time cron "0 0 0 ? * 2#1"
then
vLastMonth_Power_Bill.postUpdate(vProjected_Power_Bill.state.toString)
aPowerSensor_Reset.sendCommand(ON)
logInfo("utilities", "It is the first Monday of the week, resetting the power meter values.")
end
rule "Update bill estimate"
when
Item vElectricMeter_kWh changed
then
if(vElectricMeter_kWh.state == NULL || vElectricMeter_kWh.state == UNDEF) aInfo.sendCommand("The power meter has gone offline")
else if(vProjected_Power_Bill.state == NULL) vProjected_Power_Bill.postUpdate(gridAccess)
else vProjected_Power_Bill.postUpdate(gridAccess + ((vElectricMeter_kWh.state as Number) * rate))
end
Python
from core.rules import rule
from core.triggers import when
@rule("Power Meter Reset", description="On the first Mon of the month reset the power meter total KWh", tags=["utilities"])
@when("Time cron 0 0 0 ? * 2#1")
def reset_powermeter(event):
events.postUpdate("vLastMonth_Power_Bill", items["vProjected_Power_Bill"])
events.sendCommand("aPowerSensor", "ON")
reset_powermeter.log.info("It is the first Monday of the month, resetting the power meter values.")
from core.rules import rule
from core.triggers import when
from personal.util import send_info
rate = 0.1066
gridAccess = 34.0
@rule("Powerbill Estimate", description="Makes an esitmate of the upcoming power bill based on measured usage.", tags=["utilities"])
@when("Item vElectricMeter_kWh changed")
def update_powerbill(event):
if isinstance(event.itemState, UnDefType): send_info("The power meter is offline!")
elif isinstance(items["vProjected_Power_Bill"], UnDefType): events.postUpdate("vProjected_Power_Bill", gridAccess)
else: events.postUpdate("vProjected_Power_Bill", str(gridAccess + event.itemState.floatValue() * rate))
This Rule is a good example for how to do math with Item states.
More Lessons Learned
-
It is not clear to me when and why I need to use global for my presence_timer, for example, but the example in the Helper Library docs does not and I don’t have to for rate and girdAccess in the last Rule
-
When in trouble, make sure to add the log_traceback and perhaps even a try/except to your Timer bodies to get at a usable error. Otherwise it get’s buried by a Java Exception.
-
Expire based Timers do not do much to help simplify your Rules with Jython. You have to make the body of the Timers a separate function anyway and in fact, it’s more complex because to us Expire based Timers you would have to add all the Rule decorators. So you will see that all the Timers I had will go away.
Previous: Journey to Python 6 of 9
Next: Journey to JSR223 Python 8 of?