A rule idea using metadata

This is still a work in progress for me, but I’d like to hear comments / suggestions from others.

EDIT: I’ve renamed it again to “gItemRule”. The code can be found at https://github.com/jimtng/openhab-rules
It is not yet fully usable because it refers to some helper functions / class inside other files that I haven’t published yet.

I have been using the “Associated Item” design pattern for quite some time, ever since I became aware of it. However I still find myself writing many rules that seem similar with just a few tweaks here and there.

To increase the flexibility and having just one rule to handle all the different permutations (so far) or a similar task of “if something happens to one thing, send command to other item(s)”, I have implemented a different system, which allows me to trigger other items and specify the condition on per item basis, whilst still using the same rule.

The biggest difference between this vs the Associated Item design pattern is that this can:

  • Send command (or postupdate) to multiple items, without needing to follow any specific naming pattern.
  • Include the specific conditions that would trigger that command/update to that item, taking out this logic from the actual rule code, thus making the rule code generic and can be used in many situations / needs
  • Essentially what to trigger and when / whether to trigger things is defined in the item as an item metadata

So I came up with this syntax which is shown in the examples below.

So here’s my scheme:

  1. Motion sensor to trigger two different lights, with a different condition for each light.
Contact FrontPorch_Motion (gItemRule) { channel="xxx", gItemRule="on" [ OPEN="(? items.FrontPorch_Lux.intValue() < 10 ?)FrontPorch_Light=ON,FrontDoor_Light=ON" ] }

What this does should hopefully be obvious, but here’s the explanation:

  • If the FrontPorch_Motion item received update “OPEN”, it will:
    – send command ON to FrontPorch_Light - if FrontPorch_Lux < 10
    – send command ON to FrontDoor_Light (this is regardless of the lux condition which only applies to FrontPorch_Light.)
  1. Same as above, but the condition is not fulfilled, do not execute the rest, i.e. do not turn on any lights
Contact FrontPorch_Motion (gItemRule) { channel="xxx", gItemRule="on" [ OPEN="(? items.FrontPorch_Lux.intValue() < 10 ?),FrontPorch_Light,FrontDoor_Light" ] }

What’s the difference? One single comma separating the “condition” and the item. In this case, the condition is a “stand alone” element in the comma separated list, and when that’s the case, stop processing the remainder of the list.

Also here, the ON command is omitted, as it defaults to ON when not specified

  1. Apply a different rule for the second item
Contact FrontPorch_Motion (gItemRule) { channel="xxx", gItemRule="on" [ OPEN="(? items.FrontPorch_Lux.intValue() < 10 and items.Halloween_Light != ON ?)FrontPorch_Light,(? items.StayInTheDark_Flag != ON ?)FrontDoor_Light" ] }

In this case, Front Porch light gets turned on based on Lux level and whether the Halloween light is on, but the Front Door light gets turned on only based on a completely different criteria

  1. Use this to handle a multi press button:
String MainBathRoom_Button (gItemRule) { channel="xxx", gItemRule="on" [ SINGLE="MainBathRoom_Light_Dimmer=100,MainBathRoom_Light_Power=TOGGLE", DOUBLE="MainBathRoom_Light_Dimmer=1,MainBathRoom_Light_Power=ON", TRIPLE="MainBathRoom_Light_Dimmer=50,MainBathRoom_Light_Power=ON", QUAD="MainBathRoom_Light_CT=TOGGLE(0,100),MainBathRoom_Light_Power=ON", HOLD="MainBathRoom_Light_Switch=TOGGLE" ] }

I hope by now you can understand what that does. Note that in the case of “HOLD”, it is sending a command to a different item.

  1. And if you want to do a bit of hacking:
String MainBathRoom_Button (gItemRule) { channel="xxx", gItemRule="on" [ SINGLE="(? call_a_custom_function_here() ?)" ] }

Although of course you could just write a separate rule for this, in this case.

  1. Or if you want to execute the commands after a period of time:
Contact LoungeRoom_Motion (gItemRule) { channel="xxx", gItemRule="1h" [ CLOSED="LoungeRoom_Light=OFF" ] }

In this case, turn off the LoungeRoom Light 1 hour after no motion was detected. When motion is detected before that, another OPEN/CLOSED will be sent by the motion detector, which will trigger this rule again. It will extend the timer to 1hr from the last CLOSED update.

  1. What if I wanted to combine immediate trigger + delayed trigger?
Contact FrontPorch_Motion (gItemRule) { channel="xxx", gItemRule="on" [ OPEN="FrontPorch_Light=ON" ], gItemRule2="5m" [ CLOSED="FrontPorch_Light=OFF" ] }
  1. OK What if I wanted to do something now, something else 5 mins later, and something else 15 mins later?
Contact FrontPorch_Motion (gItemRule) { channel="xxx", gItemRule="on" [ OPEN="FrontPorch_Light=ON" ], gItemRule2="5m" [ CLOSED="FrontPorch_Light=OFF"], gItemRule3="15m" [ CLOSED="gAllLights=OFF" ] }
  1. Don’t want to repeat the same rules over and over again, say you have multiple buttons that are supposed to do exactly the same thing?
String DummyItemTemplate (gItemRule) { gItemRule="on" [ SINGLE="(? items.MainBathRoom_Light_Power != ON ?)MainBathRoom_Light_Dimmer=100,MainBathRoom_Light_Power=TOGGLE", DOUBLE="MainBathRoom_Light_Dimmer=1,MainBathRoom_Light_Power=ON", TRIPLE="MainBathRoom_Light_Dimmer=50,MainBathRoom_Light_Power=ON", QUAD="MainBathRoom_Light_CT=TOGGLE(0,100),MainBathRoom_Light_Power=ON", HOLD="MainBathRoom_Light_Switch=TOGGLE" ]  }

// Just use the same rules from DummyItemTemplate
Button1 (gItemRule) { gItemRule="on" [ __same_as__="DummyItemTemplate" ] }

// But override one thing... let's disable HOLD on this item, make it do nothing
Button2 (gItemRule) { gItemRule="on" [ __same_as__="DummyItemTemplate", HOLD="" ]
  1. Want to turn off lights after a period of no motion being detected? Add this to the motion sensor item:
Contact StudyRoom_Motion      "Study Sensor"          (gItemRule) { 
    gItemRule="on" [
        OPEN="(? cancel_timer('StudyRoom_Motion') ?)"
    ],
    gItemRule2="1h" [
        __condition="items.StudyRoom_Light_Switch==ON",
        CLOSED="StudyRoom_Light_Switch=OFF"
    ]
}

** But what if I wanted to copy the same rules, but change the items they’re operating on… hmmm I’m still working on that.

All this is achieved by the same rule that handles “when Member of gItemRule received update”

As I said, this is still a work in progress, still tweaking things here and there.

The downside? The item definition gets a bit more complicated.

PS: I am aware of @5iver’s Area Trigger system, and at first I wanted to use it, but ended up with this instead. For now…


Rule syntax:

Performs simple commands / updates:
General syntax:
    Rule                Just do one thing
    PART1, PART2, ...   Multiple rule PARTS, Do several things in succession

General syntax for simple commands: 

Examples:
    Item1           sendCommand('Item1', 'ON')
    Item1=ON        ditto
    Item1:ON        postUpdate('Item1', 'ON') using a colon instead of an equal sign
    Item1=TOGGLE    Toggles between ON/OFF. If the current item state is neither ON nor OFF, use ON. 
                    TOGGLE is the shorthand for TOGGLE(ON,OFF)
    Item1=TOGGLE(OFF,ON)
                    Toggles between OFF/ON. If the current item state is neither ON nor OFF, use OFF (the first option in the list)

    Item1=CYCLE(RED,GREEN,BLUE)
                    Cycles the value to the next one on the list based on the current item state
                    e.g. if Item1.state is currently GREEN, sendCommand('Item1', 'BLUE')
                    If the Item1.state is not in the given list, set it to the first item on the list (i.e. RED)

    TOGGLE and CYCLE are synonymous and are interchangeable. Item1=TOGGLE is the same as Item1=CYCLE which is 
    the same as TOGGLE(ON,OFF)


The simple commands/update can be done to multiple items by separating them with a comma (See General Syntax above)
by using multiple PARTS. Each part is separated by a comma

    Item1,Item2     sendCommand('Item1', 'ON') followed by sendCommand('Item2', 'ON')
    Item1:ON,Item2  postUpdate('Item1', 'ON') followed by sendCommand('Item2', 'ON')
    

Conditionals or code execution:
Python code can be executed before performing the simple commands above. 
If the resulting code evaluates to False, stops rule, depending on whether the conditional is followed by a simple command
within the same rule segment

Syntax 1: xxxxxx ?:
    items.Item1 == ON ?: Item2=ON

Explanation:
    if eval (items.Item1 == ON) execute 'Item2=ON' which means sendCommand('Item2', 'ON')

Equivalent syntaxes:
Syntax2: if xxxx :
    if items.Item1 == ON: Item2=ON
Syntax3: (? xxxx ?)
    (? items.Item1 == ON ?) Item2=ON

Examples:
    if items.Item1 == ON: Item2=ON
        Execute Item2=ON only if the code "items.Item2 == ON" evaluates to true
    
    if items.Item1==ON: Item2=ON,Item3=ON
        Same as above, but set Item3=ON regardless of what happened in the first PART

    (? items.Item1==ON ?),Item2=ON,Item3=ON
        Notice the conditional is not followed by a simple command. 
        The command is inside the next PART separated by a comma. 

        In this instance, if the conditional returns False, stop execution of the rest of the parts
        In other words, if items.Item2 is not ON, do not execute Item2=ON and Item3=ON

        Such conditional can occur in the middle of the list of PARTS too, e.g.

    Item2=ON,Item3=ON, (? items.Item1==ON ?), Item4=ON
        In this case, Item2 will be set to ON, Item3 will be set to ON, then the conditional will be evaluated, and its 
        result will only affect what follows after it.

        The same rule applies on whether it has a simple rule within the same part or it is stand alone, 
        immediately followed by a comma

Note that all three syntaxes are equivalent. So these three are the same:
    if items.Item1 == ON:   Item2=ON
    items.Item1 == ON  :?   Item2=ON
    (? items.Item1 == ON ?) Item2=ON

White spaces outside conditionals are ignored, so these are the same:
    Item1:ON,Item2=TOGGLE(A,B,C)
    Item1:ON, Item2 = TOGGLE(A, B, C)
    Item1 : ON , Item2 = TOGGLE (  A , B , C )
1 Like

Thinking further about the delayed rule, it’s a bit tricky about cancelling the timer.

Considering this scenario:

  • Motion1 detects motion, turns on Light1 and sets a timer to turn it off in 10 minutes.
  • Meanwhile, a person turns off Light1, then a moment later, turns it back on. This time the person expects the light to stay on, but meanwhile that 10 minute timer is still running.

EDIT: I’ve figured out the solution using a helper function. When Light1 is turned off, cancel the timer created by Motion1’s rule, if it’s still active

// Motion detected, turn on Light 1, and In 10 minutes turn it off
Contact Motion1 (gSimple_Rule) { gSimpleRule="on" [ OPEN="Light1=ON" ], gSimple_Rule2="10m" [ CLOSED="Light1=OFF" ] } 

// When Light1 is turned off (e.g. manually or even due to the timer), cancel the timer created by Motion1 above
Switch Light1 (gSimple_Rule) { gSimple_Rule="on" [ OFF="(? cancel_timer('Motion1') ?)" ] )

This works because the timers are kept internally by using the metadata name and the item name, so the helper function cancel_timer knows how to cancel the right timer for item Motion1 created by gSimple_Rule.

1 Like

I don’t know if you noticed but a couple of my submissions to the helper libraries has some generic code to parse these types of time Strings. See the “Parse Time” function at https://github.com/openhab-scripters/openhab-helper-libraries/pull/283/files.

crazyivan added some code that periodically regenerates a Rules triggers for https://github.com/openhab-scripters/openhab-helper-libraries/pull/259 which might be useful for something like this. Without it, you need to reload the rules files when you change the membership of the Groups that trigger the Rules.

1 Like

Wow THANK YOU! I came across this head-scratcher and resorted to restarting openhab :smiley:

Indeed, I am using your parse_time function, which I saw in your expire replacement. I also created a helper function:

def parse_time_to_seconds(time_str):
    td = parse_time(time_str)
    if not td: 
        return None #should we raise an exception instead?
    return td.days * 86400 + td.seconds

This can be passed on to DateTime.now().plusSeconds(parse_time_to_seconds(‘xxxx’)) without having to remember / know anything about timedelta.

Another version for millis can be created for people who need sub-second precision I suppose.

Your parse_time() - and perhaps parse_time_to_seconds() should be included in core.date (in the helper libraries), as it’s very handy! For the time being, I’m putting it in my personal.utils.py

1 Like

I like your idea. Would you min posting your code.

I will post it on github once it’s a bit more stable. Right now it keeps changing wildly. If you don’t mind it changing around, I can post it sooner.

I have enough time with that. Post i whenever you think it is ok.

I have posted the code on github. Please check the original post above. Note that it isn’t usable / self contained as it is. It is referring to utils.py and timers.py - two other files that I have. I should clean it up and post them up as well. But for now you can take a look at the code.

Thanks a lot for your code. I build something much simpler based on your ideas as a replacement for my lighting trigger rules.

i just recognized that i need? a delay option to set the sate of an item.