Journey to JSR223 Python 3 of 9

This post is a bunch of simple little Rules which rounds out the transfer from my admin.rules file.

Send a system status alert every morning and when OH restarts

Rules DSL

rule "Reminder at 08:00 and system start"
when
          Time cron "0 0 8 * * ? *" or
          System started
then
    val numNull = gSensorStatus.members.filter[ sensor | sensor.state == NULL ].size
    if( numNull > 0) logWarn("admin", "There are " + numNull + " sensors in an unknown state")

    val offline = gSensorStatus.members.filter[ sensor | sensor.state == OFF ]
    if(offline.size == 0) return;

    val message = new StringBuilder
    message.append("The following sensors are known to be offline: ")
    offline.forEach[ sensor |
        var name = transform("MAP", "admin.map", sensor.name)
        if(name == "") name = sensor.name
        message.append(name)
        message.append(", ")
        gOfflineAlerted.members.filter[ a | a.name==sensor.name+"_Alerted" ].head.postUpdate(ON)
    ]
    message.delete(message.length-2, message.length)

    aInfo.sendCommand(message.toString)
end

Get the list of Items that are OFF and build a message listing those Items and alert. Set the alerted flag to ON.

I’ve not touched this Rule in years and as written it could be improved some.

Python

from core.rules import rule
from core.triggers import when
from core.metadata import set_metadata, get_key_value
from personal.util import send_info

@rule("System status reminder", description="Send a message with a list of offline sensors at 08:00 and System start", tags=["admin"])
@when("Time cron 0 0 8 * * ? *")
@when("System started")
def status_reminder(event):
    status_reminder.log.info("Generating daily sensor status report")

    numNull = len(filter(lambda item: isinstance(item.state, UnDefType), ir.getItem("gSensorStatus").members))
    if numNull > 0: status_reminder.log.warning("There are {} sensors in an unknown state!".format(numNull))

    offline = filter(lambda item: item.state == OFF, ir.getItem("gSensorStatus").members)
    if len(offline) == 0:
        status_reminder.log.info("All sensors are online")
        return

    status_reminder.log.info("Building message")
    offlineMessage = "The following sensors are known to be offline: {}".format(", ".join(map(lambda sensor: "{}".format(get_key_va
lue(sensor.name, "Static", "name") or sensor.name), sorted(sensor for sensor in offline))))

    status_reminder.log.info("Updating all of the alerted flags")
    for sensor in offline: set_metadata(sensor.name, "Alert", { "alerted" : "ON"}, overwrite=False)
    status_reminder.log.info("Sending the message")
    send_info(offlineMessage, status_reminder.log)

Power Meter Status

My Zwave whole house power meter often falls offline. This little Rule converts the ONLINE/OFFLINE Thing statuses to an Online Item used by the rest of the Rules.

Rules DSL

rule "Power Meter OFFLINE"
when
    Thing "zwave:device:dongle:node7" changed from ONLINE to OFFLINE
then
    vPowerMeter_Online.sendCommand(OFF)
end

rule "Power Meter ONLINE"
when
    Thing "zwave:device:dongle:node7" changed to ONLINE
then
    vPowerMeter_Online.sendCommand(ON)
end

Python

from core.rules import rule
from core.triggers import when

@rule("Power Meter online status", description="Zwave has marked the power meter as offline", tags=["admin"])
@when("Thing zwave:device:dongle:node7 changed")
def pm_online(event):
    pm_online.log.info("Power meter Thing changed status {}".format(event.statusInfo.status))
    if event.statusInfo.status == ONLINE: events.sendCommand("vPowerMeter_Online", "ON")
    else: events.sendCommand("vPowerMeter_Online", "OFF")

Process Heartbeat

Rules DSL

I’ve a number of devices (Sonoffs, ESPEasy, my own sensorReporter scripts) that report their uptime periodically as a heartbeat. This Rule will ping the Online Item when a heartbeat is received to reset the Expire binding and keep the device/service online.

rule "Process heartbeat"
when
    Member of SensorEvents received update
then
    sendCommand(triggeringItem.name.replace("Uptime", "Online"), "ON")
end

Python

from core.rules import rule
from core.triggers import when

@rule("Process heartbeat", description="Process an uptime heartbeat message to ping the online status of a sensor", tags=["admin"])
@when("Member of SensorEvents received update")
def heartbeat(event):
    events.sendCommand(event.itemName.replace("Uptime", "Online"), "ON")

Update Items to UNDEF when the service goes offline

These are currently three different Rules. Eventually I’ll merge them into one more generic approach.

Rules DSL

rule "Hydra went offline"
when
    Item vHydra_SensorReporter_Online changed to OFF
then
    logInfo("admin", "Hydra went offline, setting relevant Items to UNDEF")
    vFrontDoor.postUpdate(UNDEF)
    vFrontDoor_Timer.postUpdate(OFF)

    vBackDoor.postUpdate(UNDEF)
    vBackDoor_Timer.postUpdate(OFF)

    vGarageDoor.postUpdate(UNDEF)
    vGarageDoor_Timer.postUpdate(OFF)
end

rule "Cerberos went offline"
when
    Item vCerberos_SensorReporter_Online changed to OFF
then
    logInfo("admin", "Cerberos went offline, setting relevant Items to UNDEF")
    vGarageOpener1.postUpdate(UNDEF)
    vGarageOpener1_Timer.postUpdate(OFF)

    vGarageOpener2.postUpdate(UNDEF)
    vGarageOpener2_Timer.postUpdate(OFF)
end

rule "Manticore sensorReporter went offline"
when
    Item vManticore_SensorReporter_Online changed to OFF
then
    logInfo("admin","Manticore went offline, setting relevant Items to UNDEF")
    vJennPhone_Manticore_Net.postUpdate(UNDEF)
    vRichPhone_Manticore_Net.postUpdate(UNDEF)
    vGJPhone1_Manticore_Net.postUpdate(UNDEF)
    vGJPhone2_Manticore_Net.postUpdate(UNDEF)
end

Python

from core.rules import rule
from core.triggers import when

# TODO: make generic
@rule("Hydra Offline", description="Sets relevant Items to UNDEF when this sensor goes offline", tags=["admin"])
@when("Item vHydra_SensorReporter_Online changed to OFF")
def hydra_offline(event):
    hydra_offline.log.warn("Hydra went offline, setting relevant Items to UNDEF")
    events.postUpdate("vFrontDoor", "UNDEF")
    events.postUpdate("vFrontDoor_Timer", "OFF")
    events.postUpdate("vBackDoor", "UNDEF")
    events.postUpdate("vBackDoor_Timer", "OFF")
    events.postUpdate("vGarageDoor", "UNDEF")
    events.postUpdate("vGarageDoor_Timer", "OFF")

@rule("Cerberos Offline", description="Sets relevant Items to UNDEF when this sensor goes offline", tags=["admin"])
@when("Item vCerberos_SensorReporter_Online changed to OFF")
def cerberos_offline(event):
    cerberos_offline.log.warn("Cerberos went offline, setting relevant Items to UNDEF")
    events.postUpdate("vGarageOpener1", "UNDEF")
    events.postUpdate("vGarageOpener1_Timer", "OFF")
    events.postUpdate("vGarageOpener2", "UNDEF")
    events.postUpdate("vGarageOpener2_Timer", "OFF")

@rule("Manticore Offline", description="Sets relevant Items to UNDEF when this sensor goes offline", tags=["admin"])
@when("Item vManticore_SensorReporter_Online changed to OFF")
def manticore_offline(event):
    manticore_offline.log.warn("Manticore went offline, setting relevant Items to UNDEF")
    events.postUpdate("vJennPhone_Manticore_Net", "UNDEF")
    events.postUpdate("vRichPhone_Manticore_Net", "UNDEF")
    events.postUpdate("vGJPhone1_Manticore_Net", "UNDEF")
    events.postUpdate("vGJPhone2_Manticore_Net", "UNDEF")

Previous: Journey to JSR223 Python 2 of 9
Next: Journey to JSR223 Python 4 of 9

EDIT: Corrections and improvements from 5iver.

4 Likes

Another way…

https://openhab-scripters.github.io/openhab-helper-libraries/Guides/But%20How%20Do%20I.html#stop-a-rule-if-the-triggering-item-s-state-is-null-or-undef

Use statusInfo.status for this. statusInfo can be ONLINE, but I’ve never seen it go to just OFFLINE.

1 Like

OK, this is a weird one.

The reporting Rule above with the cron trigger wasn’t working. I looked into it a bit more and I was receiving an error in events.log, of all places.

Originally the trigger was "0 0 8 * * ? *". This generated

2019-07-25 16:14:29.307 [.event.RuleStatusInfoEvent] - 0e382029-2a19-4caa-9bc9-27ed2efec62e updated: UNINITIALIZED (INVALID_RULE): Validation of rule 0e382029-2a19-4caa-9bc9-27ed2efec62e has failed! Invalid module uid: Time-cron-0 0 8 * * ?-92357e51af2911e9b576a972d4d66768. It is null or not fit to the pattern: [A-Za-z0-9_-]*
  • The Triggers docs implies that I don’t need the “Time cron” part.

  • Why didn’t I get an error in openhab.log?

  • I understand why it appeared in events.log since it is an event.

Another known bug that I’ve already fixed… I still haven’t pushed yet. Use ‘Time cron …’ for now.

Should have never been in there as an option. I only included it in the code to keep the same functionality as in the original libraries.

1 Like

Rich, do you mind to cross-check your “Journey” code for unicode compatibility ?
With Python 2/Jython “not there yet”, they don’t necessarily work out of the box.
Many European OH users to naturally use non-ASCII umlauts in item names/descriptions, log strings etc to come to hit this soon I guess.
The first example here bails out on a group of sensors of mine. Might be just silly me, still learning Python and not gotten to the ground but I’m guessing it’ll be unicode stuff to affect this somewhere.

I’m not sure I know how the best way is. I’m no python expert myself and about all I know about Unicode is that if you out a u before a string I think it forces it to use Unicode e.g. u"My String". @5iver, @CrazyIvan359, any ideas?

Unicode won’t get only European users but any user who uses a temperature UoM.

A quick Google search may help.

https://docs.python.org/2/howto/unicode.html

That already is the first and most often hit pitfall. I’d suggest to use u"" in your code by default.
The other implications are a bit more difficult, e.g. if you pass strings to functions, you eventually would need to convert them first (eventually using the unicode() constructor but as I said I have not gotten to ground myself).
Maybe “start simple” and artificially create items(s) to contain UTF-8 names or descriptions and then use these in your code ? So you’ll notice yourself right away.

Yes and potentially more. But UoM is more of a corner case while I fear any European user of OH to embark on this “journey” will hit this right away and it has the potential to quickly demotivate him (for him, Python is likely also all-new).

I don’t think that’s the best way though. I can’t imagine a language as popular as python would require that. There had to be a way to say “this whole file uses Unicode” or “this whole module uses Unicode”. I don’t want to change anything until I know the best way.

I thought I covered this here…

I’d need some specific examples of code that is failing. The helper libraries probably don’t need help with unicode, since so many are using them successfully. So, any issues would be in scripts or community solutions.

At times, encode and decide need to be used too. I’ve spent a bit of time working through this since your other post in order to get this documented.

I agree

That’s why Python3 changed the default! Unicode is a pain in, especially in Python2.

1 Like

So don’t use # -*- coding: utf-8 -*- because there is some bug in Jython. Use u"string" everywhere.

Yuck but if that’s how it has to be… I’ve a lot of examples to edit.

I’ve hit it when implementing right the example code of this thread, that’s why I posted it here.
Try to apply it to an item that has umlauts in its description (2nd field in .items is what I mean by that).
I’ll get back to you with more examples when I’ve managed to fully understand what’s going on.

There are several examples in this post :slightly_smiling_face:. Which line has the problem with unicode strings?

If I have the following:

a.name==sensor.name+"_Alerted"

Would a.name==sensor.name+u"_Alerted" force the whole thing to be Unicode? Or does everything really need to use format instead of string concatenation? u"{}_Alerted".format(sensor.name)? I think I’ve since moved all my code to use format anyway but these early examples of mine don’t.

The one with format...lambda (if you have non-ASCII item names or other fields to be used in the group/item data structure, I haven’t fully determined the root cause yet)

So, the System Status example… maybe this line?

What error do you get, and did you remove the errant space?

yes

yes
I did use the line below. Problem may be elsewhere, too (I’m a newbie to Python).

battery_msg = u"Batteriestände unter der Meldegrenze : {}".format(", ".join(map(lambda sensor: "{}".format(get_key_value(sensor.name, "Static", "name") or sensor.name), sorted(sensor for sensor in battery_low))))

2019-12-15 17:50:00.254 [ERROR] [jsr223.jython.battery monitor       ] - Traceback (most recent call last):
  File "/etc/openhab2/automation/lib/python/core/log.py", line 51, in wrapper
    return fn(*args, **kwargs)
  File "<script>", line 27, in battery_check
  File "<script>", line 27, in <lambda>
NameError: global name 'get_key_value' is not defined

2019-12-15 17:50:00.280 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule '4a7be6ef-8eff-4746-a86d-3f84fc03c850': Fail to execute action: 1
2019-12-15 17:50:00.282 [DEBUG] [e.automation.internal.RuleEngineImpl] -
java.lang.RuntimeException: Fail to execute action: 1
        at org.openhab.core.automation.internal.RuleEngineImpl.executeActions(RuleEngineImpl.java:1197) ~[bundleFile:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.runRule(RuleEngineImpl.java:993) [bundleFile:?]
        at org.openhab.core.automation.internal.TriggerHandlerCallbackImpl$TriggerData.run(TriggerHandlerCallbackImpl.java:91) [bundleFile:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [?:1.8.0_222]
        at java.util.concurrent.FutureTask.run(FutureTask.java:266) [?:1.8.0_222]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_222]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_222]
        at java.lang.Thread.run(Thread.java:748) [?:1.8.0_222]
Caused by: org.python.core.PyException
        at org.python.core.Py.AttributeError(Py.java:207) ~[?:?]
        at org.python.core.PyObject.noAttributeError(PyObject.java:1032) ~[?:?]
        at org.python.core.PyObject.__getattr__(PyObject.java:1027) ~[?:?]
        at core.rules$py.execute$6(/etc/openhab2/automation/lib/python/core/rules.py:110) ~[?:?]
        at core.rules$py.call_function(/etc/openhab2/automation/lib/python/core/rules.py) ~[?:?]
        at org.python.core.PyTableCode.call(PyTableCode.java:171) ~[?:?]
        at org.python.core.PyBaseCode.call(PyBaseCode.java:308) ~[?:?]
        at org.python.core.PyBaseCode.call(PyBaseCode.java:199) ~[?:?]
        at org.python.core.PyFunction.__call__(PyFunction.java:482) ~[?:?]
        at org.python.core.PyMethod.instancemethod___call__(PyMethod.java:237) ~[?:?]
        at org.python.core.PyMethod.__call__(PyMethod.java:228) ~[?:?]
        at org.python.core.PyMethod.__call__(PyMethod.java:218) ~[?:?]
        at org.python.core.PyMethod.__call__(PyMethod.java:213) ~[?:?]
        at org.python.core.PyObject._jcallexc(PyObject.java:3644) ~[?:?]
        at org.python.core.PyObject._jcall(PyObject.java:3676) ~[?:?]
        at org.python.proxies.core.rules$_FunctionRule$68.execute(Unknown Source) ~[?:?]
        at org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRuleActionHandlerDelegate.execute(SimpleRuleActionHandlerDelegate.java:34) ~[?:?]
        at org.openhab.core.automation.module.script.rulesupport.internal.delegates.SimpleActionHandlerDelegate.execute(SimpleActionHandlerDelegate.java:59) ~[?:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.executeActions(RuleEngineImpl.java:1189) ~[bundleFile:?]
        ... 7 more

That looks like a missing import. Indeed, it’s missing the import of get_key_value from core.metadata. somehow I messed up the copy and paste of that rule, or I came back later and updated without fixing the imports.

from core.metadata import set_metadata, get_key_value

That’s probably what happened because I have centralized getting an Item’s name from metadata to a library function in my production code after I posted this but I’m sure I had to come back and update some bug I found so I reverted the function call and forgot to add back the import.

Ok yes thanks for the hint, now it works. So sadly it’s not the example Scott is looking for.
But this was when I thought that if the name or some other string was used as a selector to access an array or some such it might fail when it’s not ASCII, hence my post on (non-)Unicode as a potential error source.