Compare heartbeat time against current time to check if device is alive in jython

Foreward

The default MySensors MQTT implementation does not include a Last Will & Testament feature. As a result, I need to check ‘manually’ whether each sensor is still alive.

I’ve got what I need working - I’m just not particularly impressed with my implementation, so would be happy to get feedback.

Setup

I have:

  • An Arduino Pro Mini with a DHT22 attached to it running a MySensors sketch.
  • The Pro Mini sends data every minute to a MySensors MQTT gateway.
  • The MySensors MQTT gateway publishes that data to my Mosquitto MQTT broker.

The MySensors gateway, Mosquitto and OpenHAB (2.5.5 Release Build) are all running on the same Raspberry Pi 3B+.

OpenHAB setup

mysensors.things

Thing mqtt:topic:msSpareRoom "Spare Room" (mqtt:broker:MosquittoMqttBroker) {
	Channels:
		Type number : temperature "Temperature" [ 
			stateTopic="mysensors-out/2/1/1/0/0"
		]
}

mysensors.items

Thanks to this clever shenanigans, I can use the fact that the Pro Mini sends out a regular temperature reading to create an item which stores the timestamp of the last message.

DateTime dtSpareRoomLastUpdate (gSensors) { channel="mqtt:topic:msSpareRoom:temperature" [profile="timestamp-update"]}

The item is part of the gSensors group - my intention is to create more of these sensors, adding them to the group as I go. The value that is stored by this item looks as follows:

2020-07-16T21:22:55.677+0100

rules

Now here’s where it gets a little ugly, in my opinion. All I’m trying to do is check whether the last update that I’ve received has been within the last 5 minutes, and I’m triggering this by simply checking every 5 minutes using cron.

@rule("Sensor online", description="Check if sensors are still sending data")
@when("Time cron 0 0/5 0 ? * * *")
#@when("Item sTestSwitch changed to ON")
def action_sensor_status(event):

    action_sensor_status.log.info("Checking if sensors are online")
    update_interval = 5

    for item in ir.getItem("gSensors").members:

        raw_item_name = str(item.name)
        
        if(is_alive(str(item.state), update_interval)):
            #Sensor online
            action_sensor_status.log.info(raw_item_name + " is online")
        else:
            #Sensor offline
            action_sensor_status.log.info(raw_item_name + " is offline")

def is_alive(last_change, minutes):
    utc_time = datetime.strptime(last_change[:-5], "%Y-%m-%dT%H:%M:%S.%f")
    epoch_time = (utc_time - datetime(1970, 1, 1)).total_seconds()
    current_time = (datetime.now() - datetime(1970, 1, 1)).total_seconds()

    if(current_time-epoch_time > (60*minutes)):
        #Sensor offline
        return False
    else:
        #Sensor online
        return True

I find the is_alive function ugly, with all the massaging of the dates into something useful. What I’m trying to do is convert the timestamp into seconds since the epoch, and compare that to the current seconds since the epoch. I was quite surprise that I had to do all the above just to get that to work (I understand that this should be easier with Python3, but we don’t have that yet in native openHAB).

The ugliest is having to strip the timezone offset from the timestamp ([:-5]) in order to work with strptime. A long time back I used to do quite a bit of PHP, which had a stringtotime() function into which you could throw anything and it would spit out a timestamp - I couldn’t find the same in Python2.7.

I did have a look at using openHAB’s date and time methods, but 1) I got confused very quickly and 2) I really quite like the idea of using native python for as much as possible.

So that’s it basically - what have I missed? What glaringly obvious thing should I have done instead of the spaghetti shown above? Thanks for reading to the end!

I use the https://www.openhab.org/addons/bindings/expire1/ binding and just check any sensor (in a group) for null value to achieve this. It sets an items value to null (or anything you want) if its not been updated within a certain amount of time of your choosing

Yeah, and I’ve seen the related Design Pattern too. For various reasons I’d quite like to do this without requiring extra bindings.

Weirdly, I also can’t get passed the mental image of lots and lots of countdown timers running secretly in the background, changing items when I don’t expect it. That’s more a me-problem, rather than an OpenHAB issue… :smiley:

See [Deprecated] Design Patterns: Generic Is Alive.

If you use Expire, or Jython Drop-in Replacement for Expire 1.x Binding if you don’t want to install a binding you will notice that all that needs to be done in the rule is to generate the alert. No looping through all the Items every five minutes, no complicated time comparisons, just some metadata on the Item and a Rule triggered when the Item changes to UNDEF. The rule itself will tell you what Item went offline so there is no looping required. Everything is already done for you. And you get the bonus that when OH loses the connection to the MQTT broker, it will set the Items to UNDEF too which should also generate alerts and will do so immediately instead of an hour after the fact.

You can also tune it per device/Item based on how often it reports (e.g. I’ve some battery powered Zwave smoke alarms that report only once every couple of days).

Why not just compare the times directly?

if item.state.toZonedDateTime().isAfter( DateTimeType.toZonedDateTime().minusMinutes(update_interval):
    action_sensor_status.log.info(raw_item_name + " is online")
else
    action_sensor_status.log.info(raw_item_name + " is offline")

NOTE: I just typed in the above from my phone, there may be typos.

This line is a noop. item.name is already a string.

Then you probably will be happier using HAPApp which runs outside of openHAB and interacts with openHAB through it’s REST API. It’s Python 3 to boot, though it is limited to only work with what’s exposed through the REST API (e.g. no Actions). The whole point of openHAB Rules is to interact with openHAB stuff (Items, Things, etc). Why tie your hands behind your back out of some sense of purity? As soon as you start working with Item Objects you are outside of pure Python and working with Java Objects anyway. So take advantage of all the hard work done by the developers to make writing rules like this as easy as possible. You are already not going to be using pure Python anyway.

You do realize that this is exactly what is happening with a Time cron triggered rule too, right? In OH 2.5, the timers and cron triggered rules are scheduled using the same scheduler. And in general, it’s always way better to react to an event rather than polling for an event. It takes fewer resources, is more responsive, and is much easier to implement compared to polling.

Maybe try going through the helper library documentation again :slightly_smiling_face:. core.date.minutes_between would be helpful here…

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

@rule("Sensor online", description="Check if sensors are still sending data")
@when("Time cron 0 0/5 0 ? * * *")
@when("System started")
def action_sensor_status(event):
    action_sensor_status.log.info("Checking if sensors are online")
    update_interval = 5
    for item in ir.getItem("gSensors").members:
        if minutes_between(item.state, DateTimeType()) < update_interval:
            #Sensor online
            action_sensor_status.log.info("{} is online".format(item.name))
        else:
            #Sensor offline
            action_sensor_status.log.info("{} is offline".format(item.name))

Be aware that a device may be offline for up to 9 minutes and 59 seconds when using this rule, so you may want to check more frequently or use a timer.

:-1: When using scripted automation, it is a good practice to use Java and OH classes and interfaces as much as possible, and to only use the native scripting language when there is no other alternative.

1 Like

I did look through the ‘But How Do I…’ section of the documentation, but not the core section: it seemed like a ‘scary’ area for someone who doesn’t program! I’ll try next time.

I’ve seen the use of curly braces a few times before - what’s the reason to format a string in this way, rather than just concatenate with + as I did?

That is very good feedback and I will think about ways to better shape the docs for different user levels. If you don’t give the docs a good read though, you will miss a lot of what the helper libraries can provide. Most modules contain examples too.

There are many benefits to using format. The main one being that it will do the string conversions for you, like is done by slf4j when using LogAction in the rules DSL. After a couple concatenations, you’ll also see performance benefits. IMO, it also looks a lot cleaner.

1 Like