Journey to JSR223 Python 7 of 9

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?

3 Likes

It’s a Python thing. Variables from a parent scope are read only. If you set them in the local scope you create a local variable with the same name.

def plus_one():
    one += 1
    print("plus one: " + one)

def plus_two():
    global one
    one += 2
    print("plus two: " + one)

one = 1
print("one: " + one)
plus_one()
print("two: " + one)
plus_two()
print("three: " + one)

Results in:

one: 1
plus one: 2
two: 1
plus two: 3
three: 3

So is the Timer example in the Helper Library docs wrong? Never mind, https://github.com/openhab-scripters/openhab-helper-libraries/blob/master/Script%20Examples/Python/timer_example.py

Never mind, I see now that chargerTimer2 is promoted to global in the example. My confusion was I missed that line when I first looked at it.

it’s not just a python thing. It’s in many languages like this. global and local scope.

did you try your example? plus_one() uses an unknown (local) variable “one”

From 7 months ago, I’m afraid I don’t remember. It being an unknown local variable is exactly the point I was attempting to illustrate though.

No problem. I was just looking around here…