Design Pattern: Working with Groups in Rules

Correct. DecimalType and Number do implement Comparable which is why the code runs at all. The problem is the VSCode is not able to go down the class hierarchy because it can’t know what subclass of State gBattery’s members will have.

Put another way the class hierarchy is (nominally, I’m going from memory):

    Object
    |    \
    | Comparable
    |     |
  State Number
    |     |
  DecimalType

All that VSCode will know is State. It has no way to know that gBattery has NumberItems and therefore their States will implement Comparable.

However, when the Rule runs, the Rules Engine is able to say “hey state, do you implement Comparable?” and state can reply “yes” and the code will run happily.

So it boils down to the Rules Engine having more information at runtime than VSCode can have. And since it is possible (as far as VSCode is aware) that there might be States that do not implement Comparable, VSCode is going to warn you that this code might not run (e.g. if you have Switch Items in gBatteries).

You should be able to just change to sortBy to sortBy[state as Number].

In fact, there could still be problems even if all the members of gBattery are NumberItems as it is possible for their state to be NULL which itself does not implement Comparable (I don’t think, I did not check to verify). It might be worthwhile to filter out those to avoid the Rule crashing should any of its members go to NULL.

gBattery.members.filter[bat | bat.state instanceOf Number].sortBy[state as Number].forEach...

I hope that makes sense. If not let me know.

3 Likes

Yep. Makes perfect sense. Thanks!

I thought I tried that, but vscode still complained. I’ll double check.

Good suggestion! Yeah, I was wondering about that. Shouldn’t happen, though, as gBattery is persisted.

Thanks again!

I have been experimenting with the Group:Number:SUM and it seems that it will only trigger once per changed item, if the rule is when Item gDoorSensors changed

I have a rule that looks like this:

rule "Change Heat Valve Actuator"
    when
        Item C_Heat_Valves changed
    then
        val valve = C_Heat_Valves.members.filter[s|s.lastUpdate() !== null].sortBy[lastUpdate()].last as ContactItem
        logInfo("thermostat.rules", "Setting thermostat valve actuator" + valve.name)
        
        var actuator = C_Heat_Actuators.members.findFirst[name.equals(valve.name + "_Switch")]
        if (actuator!==null){
            actuator.sendCommand(if(valve.state != OFF) OFF else ON)
        } else {
	        logWarn("thermostat.rules", "Trying to set actuator but " + item.name + "_Switch" + " is not part of group")
        }
    end

Which allows me to do one rule that covers all contact items in a group. Am I completely missing something here, reading the logs wrong? Because it seems to work. But from @rlkoshak I gathered that it would trigger 10 times when any contact changed?

The groups are defined like:

Group:Number:SUM           C_Heat_Valves           "Floor Heating Valves"    <heating>            (FF)
Group C_Heat_Actuators

received update would trigger the rule 10 times. By using changed it will only trigger when the value of the Group changes state. This is a good approach to limit how many times the Rule gets triggered for updates to its members, but this approach is not always possible (e.g. the types of the members do not allow for a Group:Number:SUM).

In theory, there is a (minimal) chance that two items will change their state at the very same moment, so the sum might stay the same.
But I think this is very unlikely to happen… :wink:

can you post the full example of the working code , I am trying to understand the syntax of this rule system

Sure, here’s the complete rule.

import java.util.Date
import java.text.SimpleDateFormat

val int NUMBER_HOURS = 8

rule "Battery Health Monitor"
when 
    Time cron "0 0 19 * * ?"
then
    var SimpleDateFormat df = new SimpleDateFormat( "MM/dd" )
    var String timestamp = df.format( new Date() )

    val String mailTo = AlertEmailAddress.state.toString
    var String subject = "Battery Health Report on " + timestamp
    val StringBuilder body = new StringBuilder("Battery health report:\n\n")

    gBattery.members.sortBy[(state as Number).intValue].forEach [ NumberItem myItem | {
        body.append(String::format("%-30s%8.4s\n", myItem.name, myItem.state.toString))
    }]
    body.append("\n\n")

    gBatteryLastUpdate.members.forEach [ GenericItem myItem | {
        if (myItem.state !== NULL) {
            var DateTime dateTime = new DateTime((myItem.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)
            if (dateTime.isBefore(now.minusHours(NUMBER_HOURS))) {
                var SimpleDateFormat lastUpdateTime = new SimpleDateFormat( "MM/dd/YYYY HH:mm:ss" )
                var long t1 = (myItem.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli
                body.append("No battery update from '")
                    .append(myItem.name)
                    .append("' in at least ")
                    .append(NUMBER_HOURS)
                    .append(" hours:\t")
                    .append(lastUpdateTime.format(new Date(t1)))
                    .append("\n")
            }
        }
    }]
    sendMail(mailTo, subject, body.toString())
end
2 Likes

not sure this is the right place, but I’ve tried to adapt the timer part of the above examples an run into problems… I have a group of DateTime items based on the associated item approach, but when I try to reschedule the timer, I get the following error

'reschedule' is not a member of 'org.eclipse.smarthome.core.library.items.DateTimeItem'
            _timer = gTimerGroup.members.findFirst[id | id.name == "My_Timer_" + _name]
            if(_timer === null) {
                _timer = createTimer(now.plusMinutes(5), [|
                    // do something
                ])
            } else {
                _timer.reschedule(now.plusMinutes(5))
            }

any ideas?

Timers can’t be stored in an Item. Timers are not of type DateTime either, but of type Timer.

arrgh! of course! Thanks for pointing out the obvious :wink:

@rlkoshak - First of all: Thank your for your immensly helpful contributions in terms of scripting in openHAB

Can it be that the implementation of the Rules DSL in openHAB has changed and one needs to write lamdas with ( ) instead of [ ] now?

The reason I ask is, that one of my nicely working rules with lambdas stopped working after a reboot of OpenHAB and the reason was, that the lambda expression

gBattery.members.filter[i|i.state <= BatteryMin].forEach[i|sendCommand (BatteryLow, ON)]

suddenly triggered this Warning in the logfile

2018-03-30 13:44:54.232 [WARN ] [el.core.internal.ModelRepositoryImpl] - Configuration model 'default.rules' has errors, therefore ignoring it: [35,25]: mismatched character '|' expecting ']'
[35,49]: mismatched input ']' expecting 'end'
[35,58]: mismatched character '|' expecting ']'```

In a similar fashion Visual Studio complains now:

Invalid number of arguments. The method filter(Iterable<T>, Function1<? super T, Boolean>) is not applicable for the arguments (Set<Item>)

A change to( ) solved the issue. The working code looks like this now.

gBattery.members.filter[i|i.state <= BatteryMin].forEach[i|sendCommand (BatteryLow, ON)]

Not ( ) but you might need a space after the [ and before the ]. I’m not on the snapshot yet so don’t know the full range of where this applies. I’m only going based on a couple of threads.

Let me know if adding a space works or not.

Yes, you got it, adding a space after the [ and before the ] makes the lambda run again. ( ) can be used without spaces.

Interesting.

It is probably a minor update to Xtend that got included in ESH which got included in OH during the most recent pull of ESH into OH.

I already have to edit and rewrite about half of the DPs already with the introduction of triggringItem and Member of triggers and now this. Lots of work ahead. I’ve added a note to the OP with the warning about the spaces.

1 Like

So glad I found this thread. The space issue has caused all of my rules with lambdas to fail in the snapshots for the last couple of weeks. I had tried replacing square brackets with round brackets which allowed some of them to load, but caused other issues.

2018-04-01 09:41:06.717 [WARN ] [el.core.internal.ModelRepositoryImpl] - Configuration model 'security.rules' has errors, therefore ignoring it: [468,44]: mismatched character '|' expecting ']'
[473,11]: missing '}' at ']'
[482,8]: mismatched input ']' expecting '}'
[485,40]: mismatched character '|' expecting ']'
[493,45]: mismatched character '|' expecting ']'
[498,11]: mismatched input ']' expecting '}'
[506,8]: mismatched input ']' expecting 'end'
[838,36]: mismatched character '|' expecting ']'
[842,9]: mismatched input ']' expecting '}'
[843,38]: mismatched character '|' expecting ']'
[847,9]: mismatched input ']' expecting '}'
[859,4]: mismatched input '}' expecting 'end'
[887,42]: mismatched character '|' expecting ']'
[967,46]: mismatched character '|' expecting ']'
[971,7]: missing '}' at ']'
[994,4]: extraneous input ']' expecting '}'
[997,3]: mismatched input ']' expecting '}'
[1004,2]: mismatched input '}' expecting 'end'

This may be an issue for a lot of people in the next stable release.

This should IMHO be worthy a post in the news and important changes section, as it breaks the rules for many people after upgrading (including me :slight_smile:) . @Kai ?

3 Likes

I have a question about this:

I have a series of rules, one of which runs on a group trigger. Sometimes it does seem slow to respond (others it is instantaneous), so I have been taking a closer look at it. it does not seem to trigger multiple times though.

here is the item definition

Group:Switch homekit "Homekit Items [%s]" <house> (All)


Group Nest_Thermostat "Downstairs" <climate> (All) ["Thermostat"]

/* Insteon items */
/* oddities */
Dimmer  christmasTree_HK            "Christmas Tree [%d%%]"      <christmas_tree>  (homekit)            ["Lighting"]      
/* Porch */
Switch  frontgardenPower_HK         "Front Power [%s]"           <poweroutlet>     (homekit)            ["Switchable"]  
Dimmer porchLightKuna_HK            "Porch Light[%d%%]"                            (homekit)            ["Lighting"]     
Dimmer drivewayLightKuna_HK         "Driveway Light[%d%%]"                         (homekit)            ["Lighting"]  

/* Basement */
Switch  basementFluorescents_HK     "Basement Main Light[%s]"                      (homekit)            ["Lighting"]    
Dimmer  workshopBulb_HK             "Workshop Light[%d%%]"                         (homekit)            ["Lighting"]
Switch  workbenchLight_HK           "Workbench Light[%s]"                          (homekit)            ["Lighting"]
...
Dimmer  landingMain_HK              "Landing Light[%d%%]"                          (homekit)       ["Lighting"]

and so on, there are 51 items in total.

here is the rule:

import java.util.HashMap
import java.util.Map
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock

var ReentrantLock HomekitLock = new ReentrantLock(false)

var Map <String, GenericItem> ItemObjects = newHashMap
var Boolean homekit_init = false

rule "Homekit Item Changed"
when 
    Item homekit received update
then
    try {
        //HomekitLock.lock
        if(!homekit_init) return;
        logDebug("HomeKit","Homekit Item received update")

        homekit.members.forEach[ i | 
            //trap special functions (triggers) here - these are usually one shot events, that don't have a "state" as such
            //ie trigger them with "ON" (unless you write the rule such that they set themselves OFF after Triggering)
            switch(i.name) {
                default               : {  //put something here if needed
                                        }
            }
            var realitem_name = i.name.split("_HK").get(0)  //get realitem name
            var realitem = ItemObjects.get(realitem_name)   //get realitem object
            var realitem_state = realitem.state
            //logInfo("HomeKit", "processing: " + realitem.name + " state: " + realitem_state)
            switch(realitem.name) {
                case "DIRECTsequence" : if(realitem_state == NULL)
                                            realitem_state = OFF
                                        else if(Integer::parseInt(realitem.state.toString) > 2)
                                            realitem_state = ON
                                        else 
                                            realitem_state = OFF
            }
            if(realitem_state != i.state && i.state != NULL) {  //if HK state != current state (and not NULL)
                //logDebug("HomeKit", "FOUND!!!! Item: " + i.name + " state: " + i.state + " is not the same as " + realitem.name + " state " + realitem.state)
                logDebug("HomeKit", "FOUND!!!! Item: " + realitem.name + " updating to: " + i.state)
                //handle special case triggers
                //logDebug("HomeKit", "Processing " + i.state + " trigger for: " + i.name)

                var command = i.state.toString  //command to send
                var commanditem = realitem.name //item to send it to
                switch(realitem.name) {
                    case "DIRECTsequence"   :   if(i.state == ON) 
                                                   command = "8"
                                                else 
                                                   command = "2"
                    case "Skype_Answer"     :   if(i.state == ON)
                                                    command = "KEY_ENTER"   //send enter to answer SKYPE
                                                else
                                                    command = "KEY_EXIT"    //send exit to hang up
                    case "RGBWControllerW"  :   if(i.state == 100)
                                                    command = "10"  //set to 10% with ON command
                    case "TV_Volume"        :   {if(i.state == 0) {
                                                   commanditem = "TV_Mute"
                                                   command = "ON"
                                                }
                                                else if (i.state == 100) {
                                                   commanditem = "TV_Mute"
                                                   command = "OFF"
                                                }}
                }
                sendCommand(commanditem, command)
                logDebug("HomeKit", "Sent Command to realitem: " + commanditem + " to " + command)
            }
        ]
        logDebug("HomeKit","End of Homekit Item Received Update rule")
    }
    catch(Throwable t) {
        logError("HomeKit", "Error in Homekit Item Changed: " + t.toString)
    }
    finally {
        // always runs even if there was an error, good place for cleanup
        //HomekitLock.unlock
    }
end

As you can see I tried messing with locks to see if it was triggering multiple times, but it doesn’t seem to be. This works just fine, just sometimes there is a weird delay.

Here is some output from my log:

20-Apr-2018 18:15:03.975 [DEBUG] [ulation.internal.HueEmulationServlet] - sending 0 to landingMain_HK
20-Apr-2018 18:15:04.044 [DEBUG] [ulation.internal.HueEmulationServlet] - 192.168.100.28: GET /api/YYIxsz94Jbsz8FaviC27PZyhi7jf4uZ5Ynf72iKU/lights/25
20-Apr-2018 18:15:05.976 [DEBUG] [ulation.internal.HueEmulationServlet] - 192.168.100.187: GET /api/7b456dd4-1424-4347-9a2b-d3d8f8b3312d/lights
20-Apr-2018 18:15:16.136 [DEBUG] [ulation.internal.HueEmulationServlet] - 192.168.100.187: GET /api/7b456dd4-1424-4347-9a2b-d3d8f8b3312d/lights
20-Apr-2018 18:15:20.968 [DEBUG] [lipse.smarthome.model.script.HomeKit] - Homekit Item received update
20-Apr-2018 18:15:20.972 [DEBUG] [lipse.smarthome.model.script.HomeKit] - FOUND!!!! Item: landingMain updating to: 0
20-Apr-2018 18:15:20.972 [DEBUG] [lipse.smarthome.model.script.HomeKit] - Sent Command to realitem: landingMain to 0
20-Apr-2018 18:15:20.978 [DEBUG] [lipse.smarthome.model.script.HomeKit] - End of Homekit Item Received Update rule
20-Apr-2018 18:15:21.282 [DEBUG] [lipse.smarthome.model.script.HomeKit] - Real Item changed Rule
20-Apr-2018 18:15:21.291 [DEBUG] [lipse.smarthome.model.script.HomeKit] - End of Real Item Changed rule

homekit_init is set to true during system startup.

Notice “sending 0” is at 18:15:03.975, and the rule is triggered (“Homekit Item received update”) at 18:15:20.968 - ie 17 seconds later. I can see the event in the events log at 18:15:03:

2018-04-20 18:15:03.976 [ome.event.ItemCommandEvent] - Item 'landingMain_HK' received command 0
2018-04-20 18:15:03.976 [vent.ItemStateChangedEvent] - landingMain_HK changed from 100 to 0

There are no ‘homekit’ group events in the events log though.
However, just a bit later:

20-Apr-2018 18:28:56.526 [DEBUG] [ulation.internal.HueEmulationServlet] - sending 0 to landingMain_HK
20-Apr-2018 18:28:56.526 [DEBUG] [lipse.smarthome.model.script.HomeKit] - Homekit Item received update
20-Apr-2018 18:28:56.530 [DEBUG] [lipse.smarthome.model.script.HomeKit] - FOUND!!!! Item: landingMain updating to: 0
20-Apr-2018 18:28:56.530 [DEBUG] [lipse.smarthome.model.script.HomeKit] - Sent Command to realitem: landingMain to 0
20-Apr-2018 18:28:56.532 [DEBUG] [lipse.smarthome.model.script.HomeKit] - Real Item changed Rule
20-Apr-2018 18:28:56.537 [DEBUG] [lipse.smarthome.model.script.HomeKit] - End of Homekit Item Received Update rule
20-Apr-2018 18:28:56.544 [DEBUG] [lipse.smarthome.model.script.HomeKit] - End of Real Item Changed rule

I don’t know what may be causing the variability (unless the rule is running 50 times and I just can’t see it for some reason). I’m running OH2.2 snapshot from a few weeks ago, on an HP Proliant gen8 4GHz 4 core server, so plenty of horsepower.

Any ideas as to what is the problem? if the rule really does run 50 times, how would I see that in the logs? I can use locks to prevent it running twice, but then I might miss a real event, so I’m trying to avoid that.

Any suggestions are appreciated.

I assume you mean OH 2.3 snapshot? If so use the new Member of homekit received update rule trigger. The new implicit variable triggeringItem will be set to the Item that triggered the Rule and the Rule will only be triggered once.

I can’t answer anything about the behavior you are seeing but since the new Rule trigger solves it I’m not sure it is worth trying to figure it out.

But I will say the following:

  • If you have 51 Items in that group realize that the Rule will trigger 50 times but you only have 5 instances that can execute at a given time. I would not be surprised if this alone wouldn’t be causing problems similar to what you are seeing.
  • locks will make this problem even worse.

Thanks!

I have hacked together the following rule:

rule "Test Homekit Item Changed"
when 
    Item Member of homekit received update
then
    try {
        if(!homekit_init) return;
        logDebug("HomeKit","Test Homekit Item received update")

        var realitem_name = triggeringItem.name.split("_HK").get(0)  //get realitem name
        var realitem = ItemObjects.get(realitem_name)   //get realitem object
        var realitem_state = realitem.state
        //logInfo("HomeKit", "processing: " + realitem.name + " state: " + realitem_state)
        switch(realitem.name) {
            case "DIRECTsequence" : if(realitem_state == NULL)
                                        realitem_state = OFF
                                    else if(Integer::parseInt(realitem.state.toString) > 2)
                                        realitem_state = ON
                                    else 
                                        realitem_state = OFF
        }
        if(realitem_state != triggeringItem.state && triggeringItem.state != NULL) {  //if HK state != current state (and not NULL)
            //logDebug("HomeKit", "FOUND!!!! Item: " + triggeringItem.name + " state: " + triggeringItem.state + " is not the same as " + realitem.name + " state " + realitem.state)
            logDebug("HomeKit", "Test FOUND!!!! Item: " + realitem.name + " updating to: " + triggeringItem.state)
            //handle special case triggers
            //logDebug("HomeKit", "Processing " + triggeringItem.state + " trigger for: " + triggeringItem.name)

            var command = triggeringItem.state.toString  //command to send
            var commanditem = realitem.name //item to send it to
            switch(realitem.name) {
                case "DIRECTsequence"   :   if(triggeringItem.state == ON) 
                                               command = "8"
                                            else 
                                               command = "2"
                case "Skype_Answer"     :   if(triggeringItem.state == ON)
                                                command = "KEY_ENTER"   //send enter to answer SKYPE
                                            else
                                                command = "KEY_EXIT"    //send exit to hang up
                case "RGBWControllerW"  :   if(triggeringItem.state == 100)
                                                command = "10"  //set to 10% with ON command
                case "TV_Volume"        :   {if(triggeringItem.state == 0) {
                                               commanditem = "TV_Mute"
                                               command = "ON"
                                            }
                                            else if (triggeringItem.state == 100) {
                                               commanditem = "TV_Mute"
                                               command = "OFF"
                                            }}
            }
            sendCommand(commanditem, command)
            logDebug("HomeKit", "Test Sent Command to realitem: " + commanditem + " to " + command)
        }
        logDebug("HomeKit","Test End of Homekit Item Received Update rule")
    }
    catch(Throwable t) {
        logError("HomeKit", "Test Error in Homekit Item Changed: " + t.toString)
    }
end

but it doesn’t like Item Member of homekit received update I obviously don’t know the correct syntax. can you give a quick example?

And i will check my OH version, I thought I was on OH2.2 - maybe not…
Actually I seem to be running 2.2.0 release - huh? thought I upgraded a few weeks ago. must be getting senile.
I assume this only works with 2.3 - so maybe I need to grit my teeth and upgrade…

Not “Item Member of”, just “Member of”.

Indeed, Member of only exists in 2.3 snapshot from the past month or so.

To solve this problem I probably would go ahead and upgrade, after backing up everything with a plan to roll back if necessary of course.

The alternative is to live with the problem, least each of the 51 items as individual triggers, or spend the effort to make it work with the group trigger. None of these are as attractive as doing the work now to upgrade to the snapshot that you will have to do eventually anyway.