Re-alignment (Scenario Aligned) & refactoring of rules in OpenHAB 4

So after ignoring a pesky warning in the logs for quite some time about the eventual doom and deprecation of the ‘CreateTimerWithArgument’, I finally used the longer nights of our Southern Hemisphere winter to do something about it.

But I didn’t just want to replace that function, I also wanted to realign my rules away from a device-aligned structure, into a scenario aligned (playbook-like) structure.

The original device-aligned structure was not an intentional design choice I made, but as I was using UI based rules, I needed to keep all the manipulation of timers within the same UI script(s)…

So I did what any smart person would do… Look for the easy way out with some kind of new magic-wand function via the OpenHAB 4 wishlist. Funny thing is, with lots of helpful advice from @rlkoshak and @florian-h05, it turns out that what I was wishing for, already existed in a usable form within OpenHAB…

So I thought I might share what I did here, in case it helps someone else who is having similar ideas in relation to their rules. I have 2 disclaimers here:

#1 There is no right single answer for every problem, so adapt/ignore the following to suit your situation
#2 If you have spent dozen’s of hours writing 1000’s of lines of code getting your rules and timers working, don’t even look at @rlkoshak 's Openhab Rule tools… The heartache of all your code and time being replaced with simple, short rules, may be just too much to take :slight_smile:

So lets start on what I did…

  1. Create rules aligned to Scenarios
    Some examples below of what I mean by Scenario-aligned

So these examples are for when guests arrive/depart at the gate, or we arrive/depart the house.

  1. Break the actions down
    As mentioned above, I used to have large monolithic rules aligned to devices, due to the constraints of the UI based timers. Using the timermgr from OpenHAB rule tools in conjunction with the Global Cache, I no longer have this restriction, and can manipulate the same timer from different rules/scripts.
    Here is an example from one of the above rules:

I have also broken the executed script within a rule down into small function-aligned scripts - I have no idea if this is the most efficient way to run these, but it sure makes it easy as I drill-down into rules, then actions, to follow what is going on…

  1. Move Simple item manipulation to Scenes
    I previously used to have the (simple) manipulation of items in the Rule itself, but using a scene separates the behaviour somewhat, and it means you are not editing the rule to do an easy add/remove of an item (Which also reloads the rule upon save).

  2. Use global cache for timers
    So this enables the approach in items 1 & 2 above, and as an example, 2 different scripts from 2 different rules manipulating the same timer:

One version in the Gate guest arrive rule:

// This rule is used to turn on the Gate Button Lights and set a timer
// This will only turn on the button lights if night time (Astro managed item) 
// This version runs upon the Guest arriving at the Gate

var {timerMgr} = require('openhab_rules_tools');
var timers = cache.shared.get('gate_tm', () => new timerMgr.TimerMgr());

itemName = "Gate_ButtonLights";
force_itemName = "Gate_Force_ButtonLights";  
duration_itemName = "GateGuestArrive_GateButtonLightDuration";
duration = Number(items.getItem(duration_itemName).state);

var tmexpire = function(iname,forceiname) {
  return () => {
    items.getItem(iname).sendCommandIfDifferent("OFF");
  };
}

if(items.getItem("Daylight").state === "OFF")
 {
 schedtime = time.ZonedDateTime.now().plusSeconds(duration);
 items.getItem(itemName).sendCommandIfDifferent("ON");
 timers.check(itemName+"_off_tm",schedtime.toString(),tmexpire(itemName,force_itemName),true,null,itemName+"_off_tm");    
 }

FYI - I already know there are redundant arguments used in the tmexpire function, but I have re-used this across many rules, some which do use this - so just keeping it consistent

And one in the Gate guest depart rule:

// This rule is used to turn on the Gate Button Lights and set a timer
// This will only turn on the button lights if night time (Astro managed item) 
// This version runs upon the Guest departing at the Gate

var {timerMgr} = require('openhab_rules_tools');
var timers = cache.shared.get('gate_tm', () => new timerMgr.TimerMgr());

itemName = "Gate_ButtonLights";
force_itemName = "Gate_Force_ButtonLights";  
duration_itemName = "GateGuestDepart_GateButtonLightDuration";
duration = Number(items.getItem(duration_itemName).state);

var tmexpire = function(iname,forceiname) {
  return () => {
    items.getItem(iname).sendCommandIfDifferent("OFF");
  };
}

if(items.getItem("Daylight").state === "OFF")
 {
 schedtime = time.ZonedDateTime.now().plusSeconds(duration);
 items.getItem(itemName).sendCommandIfDifferent("ON");
 timers.check(itemName+"_off_tm",schedtime.toString(),tmexpire(itemName,force_itemName),true,null,itemName+"_off_tm");    
 }

On a naming convention whim (not based on science), I have chosen to create separate timermgr instances to group underlying timers:

  • gate_tm - With off timers Gate close timer, gate button and garden light off timers
  • heatpump_tm - With off/on timers for each heatpump within this instance

With the timers under these being the item name concatenated with “_off” or “_on”

  1. Move parameters into OpenHAB Items
    I know this is pretty obvious, but when I originally wrote the first version of the code, I took some shortcuts, and just threw parameters into the code (e.g. gate close duration, or light turn off). In this refactor, I moved these into OpenHAB items, and read the values from there.

This means I can (eventually) create a UI ‘Administration’ Page for such aspects become ‘user’ configurable

duration_itemName = "GateGuestDepart_GateButtonLightDuration";
duration = Number(items.getItem(duration_itemName).state);

Just remember to throw a entry for every such item into the appropriate persistence file (in my case
/etc/openhab/persistence/influxdb.persist) so it saves the value, and brings it back on a restart.

GateGuestDepart_GateButtonLightDuration : strategy = everyChange, restoreOnStartup

Summary:
So with daylight savings only days away, any further major changes to my OpenHAB system will probably need to wait until next winter. But for now:

  • I have tested the key functions that my original scripts used to perform, using these refactored versions, and they all seem to do the job very nicely
  • My log is not filled with warnings about functions being deprecated
  • I feel that the Rules/scripts etc are a lot easier to follow/maintain
  • And yes - I have no doubt I will one day find some edge-case around this approach & the interaction of these rules/timers !!

Again, I am not suggesting this is the right way to do it, but it is just one way to do it.
I also know I can further tidy optimise this code, maybe even move some into NPM, so less duplication across rules etc.

Thanks again to those who gave me the advice and support, and I hope that someone finds the above useful.

1 Like

:laughing:

That’s why I wrote them. To cause despair among the OH users. :wink: Not to mention the rule templates.

It’s good you are putting the action names and description fields to good use.

It’s not but who cares? Performance usually doesn’t matter in a home automation context (within reason) and when it does matter you will know.

However, it might be worth mentioning that if you are currently running on 32-bit OS on an ARM processor (e.g. Raspberry Pi) you could see an initial delay when this rule runs the very first time of up to 20 seconds * number of scripts. So be wary of that (there is an issue open to see if we can address that overall but preliminary tests show it’s a mere one to two seconds when running on 64-bit OS.

From a maintenance it often depends on what the rule is doing to determine how much to break up the scripts. It can be better in some cases to create one monolithic script action from a readability and maintainability perspective.

Only if that timer needs to be shared across multiple script actions/conditions. If the timer is only used by one, use the private cache. This eliminates the risk of reusing the same key somewhere and blowing away your timers accidentally which can be really tricky to track down.

It might be useful to describe how TimerMgr works. It a class whose purpose is to manage multiple timers independently each with a unique identifier. The usual use case is to have the timer key be the name of the Item that caused the creation of the timer in the first place.

It supports the usual cancel type methods which are self explanatory. The real interesting part is the check() function.

This function “checks” to see if there is a timer already created with that key (first argument). If not it creates one to go off at the defined time (second argument which can be anything supported by time.toZDT(). When that amount of time has passed, the function passed in the third argument gets called.

That’s the minimal use case. However, you can pass true as the fourth argument and if the timer with that key already exists it will be rescheduled based on the second argument.

If you pass in a function as the fifth argument, that function will be called if the timer already exists (maybe it’s an error case you want to report if the timer already exists?).

The final argument is the name of the timer which will be used in error messages generated by the timer.

I’ve made this more sane in the latest release of OHRT. The old way still works but the better way is now:

var {TimerMgr} = require('openhab_rules_tools');
var timers = cache.shared.get('gate_tm', () => TimerMgr());

Assuming that duration is a Number:Time you can just use:

var {TimerMgr} = require('openhab_rules_tools');
var timers = cache.shared.get('gate_tm', () => TimerMgr());

itemName = "Gate_ButtonLights";
force_itemName = "Gate_Force_ButtonLights";  
duration_itemName = "GateGuestArrive_GateButtonLightDuration";

var tmexpire = function(iname,forceiname) {
  return () => {
    items[iname].sendCommandIfDifferent("OFF");
  };
}

if(items.getItem("Daylight").state === "OFF")
{
  items[itemName].sendCommandIfDifferent("ON");
  timers.check(itemName+"_off_tm",items[duration_itemName], ,tmexpire(itemName,force_itemName),true,null,itemName+"_off_tm");    
}

Also notice I use one of the somewhat new ways to get to the Item. It makes the code a little more concise than using getItem() all the time.

If it’s just a Number Item you could use something like:

"PT"+duration+"S"

which formats the time into an ISO8601 duration string.

time.toZDT() is IMO an underrated feature of the library (I’m biased of course). You almost never need to mess with plusThis or minusThat any more. Examples:

Call Equivalent Note
time.toZDT() time.ZonedDateTime.now()
time.toZDT('PT1H2M3S') time.ZonedDateTime.now().plusHours(1).plusMinutes(2).plusSeconds(3)
time.toZDT(items.MyDTItem) time.ZonedDateTime.parse(items.MyDTItem.state)
time.toZDT(items.DurInSeconds) time.ZonedDateTime.now().plusSeconds(items.DurInSeconds.numericValue) DurInSeconds is a Number:Time
time.toZDT(items.DurInSeconds.int * 1000) time.ZonedDateTime.now().plusSeconds(items.DurInSeconds.quantityType.toUnit('s').int) DurInSeconds is a Number, a Number Item, DecimalType, or plain number passed to time.toZDT() is treated as plusMillis()
time.toZDT('08:00') time.ZonedDateTime.now().withHour(8).withMinute(0).withSecond(0).withNano(0)`
time.toZDT('1:00 pm') time.ZonedDateTime.now().withHour(13).withMinute(0).withSecond(0).withNano(0)`

As I mentioned above, all my OHRT classes that deal with times will accept anything supported by time.toZDT() which makes it much clearer and frees you from needing to convert stuff like Item states or a static times to a long string of adds and withs.

Another option would be to use Item metadata. I’ve a rule template that can be called directly from a Widget that can let you modify Item metadata so you’d still have the option to build the admin panel but it can sometimes be easier to represent configuration parameters in metadata than in a collection of Items.

One thing you might be able to use here is deferred. This is mostly useful in cases where you are simply commanding an Item in your timer body. Internally it’s implemented using TimerMgr but it’s a simpler interface because all you have to do is pass the Item name, when, and command. No rescheduling or flapping or anything like that.

2 Likes

@glen_m Very nice to see what you built using JS Scripting and openhab_rules_tools!

@rlkoshak I wonder if we have a category to somehow showcase openHAB rule related stuff, IMO this would provide some good inspiration when writing own rules.

1 Like

In the past Tutorials and Examples: Solutions have been used for this and, now that I look, I see this post isn’t already there. I’ll move it.

I could see maybe a separate “rules” category being placed under Tutorials and Examples, but there are so many rules examples there already it might be a big job to move them and it might be confusing if we don’t move them.

2 Likes

Thanks so much @rlkoshak for the extensive feedback

That’s what I always suspected - Now I finally have it in writing :slight_smile: - You certainly come across as someone who was raised on a ZX81 and was more than comfortable writing an extensive program in 1K worth of Memory space… !!

I’m working my way through your feedback and great ideas in there thanks - I will update the original post to incorporate those for completeness, once I have finished. Winter came back for a quick visit, so I managed to find the ‘inside’ time to knock out about 3/4 of the changes so far…

Fully agree - I have come a long way since the ‘CubieTruck 3’ that I originally ran OH1.8 on, and decided to turn the hardware-spec dial up a few times over the years - Now running on a AMD Ryzen 7 5800U as my home automation server. I do run more than just OpenHAB on there (e.g. XWIKI, EmonCMS and their associated DB’s), but I find that OpenHAB, and the other apps are extremely responsive… Until you are using them remotely from the other end of my crappy Rural broadband connection :frowning: But that’s a new post for another day, looking for ideas to minimise traffic/improve caching.

I have managed to set custom metadata (via item settings) & then grab the metadata via JS. I’m probably going blind, but I cannot find the rule template you mention for setting Metadata from a Widget - tried a variety of searches too… Any hints where I can find this?

Cheers - Glen

Not quite. I got started coding Breakout on an Apple IIe (when not playing Oregon Trail) and copying programs in BASIC from magazines into a TI-91 (when not playing Hunt the Wumpus). But I’ve been around long enough that I like concise and maintainable code and try to help people help their future selves by improving their code when I can (it helps the forum too not to mention the inevitable LLM AI chatbots who will be training on this code). :wink:

But my philosophy is that every program has two audiences, the computer that has to execute it and the humans that have to read and understand it. Far too few consider that latter audience and that audience is arguably more important as the human’s time is more valuable than the computer’s.

I look forward to that post but you might be limited in what you can do with a bad connection. It takes what it takes to get a message across a bad network.

It might increase latency, but MQTT was designed for this sort of environment so you might be able to get higher reliability but increased latency by using MQTT with QOS 1 or 2 to link up a remote and local OH instance. The MQTT Event Bus rule template could be used, though it might need modification to publish at these higher QOS levels.

Oh, that’s right, it’s not generic but specific to expire. Expire Updater [4.0.0.0;4.9.9.9]

Oh well, if nothing else it can show you how to do it. Sorry if I sent you on a snipe hunt.

2 Likes

Cheers for that.

The AI bots will need to suck up a LOT of your rules to fill a Large Language Model, given your concise coding :slight_smile:

Perfect - Thanks - Found the appropriate section in there, but in all honesty, I think I could just about use it in its entirety, by just modifying it to allow passing of another parameter for the Metadata item name, for which I want to update the duration (instead of expire) - Can handle that part ok (I think!!)

Just trying to get my head around the UI widget side of things now. Maybe I am getting ahead of myself here, but what I was exploring tonight was:

  • On opening, Widget calls a (new) rule with the itemname and metadata name, which then reads the metadata, and passes back its value to the widget, which is stored in a widget variable
  • User can modify existing widget value, and clicks tick (save)
  • The action is then executed to call the modified expire_updater rule with itemname, metadata name, and value
  • Live happily ever after

From reading through some posts on the forum, it looks like there is only a rule action option upon clicking the save, and no option to execute a different rule upon opening the rule to bring in a value.

I did see your feedback on https://community.openhab.org/t/send-data-from-rule-to-widget/149550, but that more appears to be looking to ‘push data’ from a rule into a widget… My thinking was to ‘pull data’ from a rule into the widget on opening.

Not so concerned about ongoing updates and the like - The only thing I would expect to be updating the metadata is this Widget itself. (Or via settings in Main UI, and if I happened to be doing both at the same time, I deserve whatever carnage ensues!!)

Problem is, from what I can see, I think bullet point 1 from above does not exist in Main UI- ‘Executing a rule upon opening/starting a widget’

Again - Probably getting ahead of myself here - already written some test rules to pull the various timer duration from metadata I created, so perhaps step 1 is update all my rules to use that, and just manage it via item settings, then worry about a nice way to present that to a user for editing etc…!!

Cheers

This isn’t possible. Only the oh-repeater widget has access to Item metadata and that widget doesn’t fit with this use case.

Correct.

A rule doesn’t really have any information to pull. You’d want to pull the Item metadata but, Item metadata is not available except in the oh-repeater widget.