This post is the conversion of my entryway Rules.
Garage Door Openers
I have a Proxy Item and a Rule that stands between the command to open the garage door openers and the MQTT message that causes it to open. I do this so I can generate an alert in case the device that controls the relays is offline to get some positive feedback that the command was received.
Rules DSL
rule "A Garage Opener was triggered"
when
Member of aGarageOpeners received command
then
var topic = triggeringItem.name + "_Cmd"
sendCommand(topic, "ON")
logInfo(logName, "Garage opener " + triggeringItem.name.substring(triggeringItem.name.length-1) + " was triggered.")
if(vNetwork_cerberos.state == OFF) {
aAlert.sendCommand("Attempting to trigger a garage opener but the controller is offline!")
}
end
When the proxy Item receives a command, forward that on the the MQTT object and then send an alert if the garage controller is offline.
Python
from core.rules import rule
from core.triggers import when
from personal.util import send_alert, get_name
@rule("Garage opener triggered", description="One of the garage door openers was triggered", tags=["entry"])
@when("Member of aGarageOpeners received command")
def trigger_garage(event):
# try to trigger the door opener
events.sendCommand(event.itemName+"_Cmd", "ON")
name = get_name(event.itemName)
trigger_garage.log.info("{} was triggered".format(name))
# alert if the opener is offline
if items["vCerberos_SensorReporter_Online"] == OFF:
send_alert("Attempting to trigger {} but the controller is offline!".format(name), trigger_garage.log)
I debated switching to using the mqttPublish Action for this but decided I like the flexibility having an Item to send the command to so if I change the topic or technology that issues the command to the relay I don’t have to change the Rule later.
Also notice how I’m using Item metadata again to get the human friendly name of the Item and I’ve moved the call to get an Item’s name from metadata to a function in my util.py library. I was uncomfortable having the namespace and key hard coded over and over across the Rules and decided to centralize that with a function call. I know that I am going to be consistent in how I define these names but the way I’m doing it right now may not be how I want to do it in the future. The method is a one liner:
def get_name(itemName): return get_key_value(itemName, "Static", "name") or itemName
I’ve changed the offline alerting Rule posted previously to use this as well.
Poll for Sensor Updates
I use a Python script that I wrote awhile back (I’m considering a refactor or rewrite) that I deploy to RPis to read their GPIO and do other stuff I can’t do from my OH server. When I initially wrote this script I wasn’t very good at MQTT and didn’t understand retained messages very well. So I coded these scripts to publish the most recent sensor readings when receiving a message on a given topic. These Rules let OH send that message so it can get the latest sensor readings from these devices.
Eventually I will change this so retained messages are used as appropriate so we don’t have to poll.
Rules DSL
rule "Request an update from the sensors"
when
Item aSensorUpdate received command
then
logInfo(logName, "Requesting sensor updates from MQTT sensors")
val mqttAction = getActions("mqtt", "mqtt:broker:broker")
mqttAction.publishMQTT("entry_sensors/getUpdate", "ON")
end
rule "A sensor device came back online, request an update from the sensors"
when
System started or
Member of sensorReporters changed from OFF to ON
then
aSensorUpdate.sendCommand(ON)
end
I implemented this in two Rules because at one point I had more than one time and condition where I would poll for the updates. I’ve since found a better way and now there is no need to do it in two separate Rules.
Python
from core.rules import rule
from core.triggers import when
@rule("Update Sensors", description="For my self build sensors, publish a message to cause them to publish the latest", tags=["admin"])
@when("System started")
@when("Member of sensorReporters changed from OFF to ON")
def update_sensors(event):
update_sensors.log.info("Requesting sensor updates from sensorReporters")
actions.get("mqtt", "mqtt:broker:broker").publishMQTT("entry_sensors/getUpdate", "ON")
In this case I did use the MQTT 2.x Action to show how it’s used.
Alerts and reminders for doors
This set of Rules is at least as complex if not more complex than my offline alerting Rules posted previously. At a high level, when a door opens or closes I create an antiflapping Timer. If we determine that the door sensor isn’t flapping (I think I have a bad wire or something, the sensor will occasionally start flapping multiple times per second for a time when it gets cold) we record the time, set a reminder Timer, and generate an alert if necessary.
Rules DSL
rule "Keep track of the last time a door was opened or closed"
when
Member of gDoorSensors changed
then
if(previousState == NULL) return;
val name = triggeringItem.name
val state = triggeringItem.state
if(state == UNDEF) {
logWarn(logName, name + " is in an unknown state!")
return;
}
// If the door changes within a second we treat it as flapping
if(antiFlappingTimers.get(name) !== null) {
logWarn(logName, name + " appears to be flapping, it's now " + state + ".")
antiFlappingTimers.get(name).reschedule(now.plusSeconds(2))
return;
}
// Set an antiflapping timer, only do the work if we prove we are not flapping
antiFlappingTimers.put(name, createTimer(now.plusSeconds(2), [ |
// Ignore the event if the door was flapping
if(triggeringItem == previousState) logWarn(logName, name + " was flapping, false alarm!")
else {
// Update the time stamp
postUpdate(name+"_LastUpdate", now.toString)
// Set the repinder timer if the door is open, cancel if it is closed
if(state == OPEN) sendCommand(name+"_Timer", "ON")
else postUpdate(name+"_Timer", "OFF")
// Set the message
val msg = new StringBuilder
msg.append(transform("MAP", "en.map", name) + " was ")
msg.append(if(state == OPEN) "opened" else "closed")
var alert = false
if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") {
msg.append(" and it is night")
alert = true
}
if(vPresent.state == OFF) {
msg.append(" and no one is home")
alert = true
}
msg.append("!")
// Alert if necessary, log if we don't
if(alert) aAlert.sendCommand(msg.toString)
else logInfo(logName, msg.toString)
}
antiFlappingTimers.put(name, null)
]))
end
rule "Timer expired for a door"
when
Member of gDoorsTimers received command OFF
then
val door = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name.replace("_Timer", ""))
if(door.state != OPEN) {
logWarn(logName, door.name + " open timer expired but door is not open!")
return;
}
val doorName = transform("MAP", "en.map", triggeringItem.name)
aAlert.sendCommand(doorName + " has been open for over an hour")
if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") {
triggeringItem.sendCommand(ON) // reschedule the timer
}
end
Notice I’m using regular Timers for the flapping and Design Pattern: Expire Binding Based Timers for the reminder.
Python
from core.rules import rule
from core.triggers import when
from personal.util import get_name, send_alert
from core.actions import ScriptExecution
from org.joda.time import DateTime
from core.log import log_traceback
from core.metadata import get_key_value
flappingTimers = {}
reminderTimers = {}
def is_night(): return items["vTimeOfDay"] == StringType("NIGHT") or items["vTimeOfDay"] == StringType("BED")
def create_timer(itemName, log):
name = get_name(itemName)
mins = get_key_value(itemName, "Static", "rem_mins").intValue() or 60
timer = ScriptExecution.createTimer(DateTime.now().plusMinutes(mins), lambda: reminder_timer(itemName, name, mins, log))
global timer
return timer
@log_traceback
def reminder_timer(itemName, name, mins, log):
if items[itemName] != OPEN:
log.warning("{} open timer expired but door is not open!".format(name))
return
send_alert("{} has been open for {} minutes".format(name, mins), log)
if is_night():
log.info("Rescheduling timer because it is night")
reminderTimers[itemName].reschedule(DateTime.now().plusMinutes(mins))
else:
del reminderTimers[itemName]
@log_traceback
def flapping_timer(itemName, origState, name, log):
if items[itemName] != origState:
log.warning("{} was flapping, false alarm!".format(name))
else:
# Update the timestamp
events.postUpdate(itemName+"_LastUpdate", DateTime.now().toString())
# Set a reminder timer
mins = get_key_value(itemName, "Static", "rem_mins").intValue() or 60
if itemName in reminderTimers and items[itemName] == OPEN:
log.info("Reschedule the timer")
reminderTimers[itemName].reschedule(DateTime.now().plusMinutes(mins))
elif itemName in reminderTimers and items[itemName] == CLOSED:
reminderTimers[itemName].cancel()
del reminderTimers[itemName]
else:
reminderTimers[itemName] = create_timer(itemName, log)
# Create the message to log and maybe alert
alert = False
change = "opened" if origState == OPEN else "closed"
time = ""
present = ""
if is_night():
time = " and it is night"
alert = True
if items["vPresent"] == OFF:
present = " and no one is home"
alert = True
msg = "The {} was {}{}{}!".format(name, change, time, present)
if alert: send_alert(msg, log)
else: log.info(msg)
# Remove the flapping timer
del flappingTimers[itemName]
# TODO: rework, these reminders are not useful any longer
@rule("Door reminder", description="Track when doors change state and set an alert if they remain open for too long", tags=["entry"])
@when("Member of gDoorSensors changed")
def door_changed(event):
if isinstance(event.oldItemState, UnDefType): return
name = get_name(event.itemName)
state = event.itemState
if state == UNDEF:
door_changed.log.warning("{} is in an unkown state!".format(name))
return
# TODO review the flapping code
# If a door changes within a second we treat it as flapping
if event.itemName in flappingTimers:
door_changed.log.warning("{} appears to be flapping, it's now {}.".format(name, state))
flappingTimers[event.itemName].reschedule(DateTime.now().plusSeconds(2))
return
# Set an antiflapping timer, ont do the work if we prove we are not flapping
flappingTimers[event.itemName] = ScriptExecution.createTimer(DateTime.now().plusSeconds(2), lambda: flapping_timer(event.itemName, state, name,door_changed.log))
@rule("Reset timers for OPEN doors", description="Reschedules the Timers for doors that are open on restart", tags=["entry"])
@when("System started")
def reschedule_door_timers(event):
for door in filter(lambda door: door.state == OPEN, ir.getItem("gDoorSensors").members):
reschedule_door_timers.log.info("Rescheduling door reminder timer for {}".format(door.name))
reminderTimers[door.name] = create_timer(door.name, reschedule_door_timers.log)
In the Python version, I decided to switch to regular openHAB Timers rather than a mix of Expire binding timers and Rule Timers. I’m also using openHAB Timers instead of Python Timers. I plan on going back and changing my alerting Rules as well.
Also, because I’ve lost the feature where my Timers get rescheduled on reboot (see previous Rule) I created a new Rule to recreate the Timers on restart for the open doors.
I decided that this is another case where using metadata makes sense to define how long to wait before reminding that the door was left open. I also use the name metadata approach.
Lessons Learned
-
Don’t be afraid to use helper functions. Unlike in Rules DSL, they do not have the same concurrency issues and are perfectly safe to use.
-
When you translate your code to JSR223, resist the urge to change it’s behavior until after you have it working like it was before. Too many changes all at once increases the chances that it will fail.
-
A lot of mistakes, typos, and other errors that would be caught by the Rules DSL parser will remain undiscovered until those lines in Python actually execute. It is vital that the code be thoroughly tested even to find some types of syntax errors.
NOTE: the above Rules have not be thoroughly been tested yet. Check back for changes over the coming days as I get a chance to test.
Previous: Journey to JSR223 Python 4 of 9
Next: Journey to Python 6 of?
Edit: Fixed numerous bugs in the code after having a chance to test it. I’m still not 100% positive the rescheudle of the timers is correct. I plan on totally reworking these Rules from scratch at some point so I’ll just keep an eye on my logs for errors.