Help converting group based xtend rule to JSR233 please

This looks like a good place to use format…

lightingPresetName = "{}_gLightingPreset{}".format(lightingItem.name, event.itemState)

Should there be another underscore after the gLightingPreset? I should have ready further… Michael already suggested this.

No need to go to the Item registry for this…

items[lightingPresetName]

Metadata is awesome, but for this purpose you might want to try adding your lighting Items to a group (like gLight) and then filter on that group.

Thanks for the tips about the string formatting and accessing items without using the registry. I did think using the item registry like that looked a bit long winded.

I do use a gLighting group in the rule. I’ll go through the code, in case it’s of benefit to anyone else trying to do the same sort of thing…

This section of the rule determines which room group the triggering item is in:

for roomGroup in ir.getItem("gRoom").members:
    # if isinstance(roomGroup, GroupItem):
    if roomGroup.type == "Group":
        for roomMember in ir.getItem(str(roomGroup.name)).members:
            if roomMember.name == event.itemName:
                triggeringRoomGroup = roomGroup

This next section loops through the room items and then the lighting items and determines if each of the lighting items is in the triggering item room group:

            for roomItem in ir.getItem(str(triggeringRoomGroup.name)).members:
                for lightingItem in ir.getItem(str("gLighting")).members:
                    if roomItem.name == lightingItem.name:
                        dev_lighting_preset_selection.log.info("NG4: Lighting item name is ({}), in room group ({}) with state ({}).".format(
                            lightingItem.name, triggeringRoomGroup.name, lightingItem.state))

At this point we’re looping around the items that are both in the triggering item room group and in the lighting group. If the items were represented in a Venn diagram we’d be looping through the items that are in the intersection of the room group that the triggering item is in and the lighting items in the same room group. Then we can build the item name of the preset (or scene as I probably should have called it) and either set the item to the preset state or turn it off:

                        lightingPresetName = "{}_gLightingPreset{}".format(lightingItem.name, event.itemState)

                        dev_lighting_preset_selection.log.info("NG4: Lighting preset item name is ({}), with state ({}).".format(
                            lightingPresetName, ir.getItem(str(lightingPresetName)).state))

                        if ir.getItem(str(lightingPresetName)).state != NULL:
                            events.sendCommand(lightingItem, items[lightingPresetName])
                        else:
                            events.sendCommand(lightingItem, OFF)

There’s a lot of looping involved in this rule but I guess in practical terms, given the processing power of the server it’s running on it doesn’t really matter. I like this approach because I can move lights around the house and move them into different presets just by changing the room group that they’re in. This one rule can be used for any of the rooms in the house due to the trigger being “Member of gLightingPresetSetting received update”.

There’s another rule, still in xtend, that deals with motion detectors, lux thresholds and timers. My next challenge! :slight_smile:

I have something you may really like and I’ll get it submitted as a community contribution in to the helper library repo this week. I’ve been testing and tweaking it for over a year now and I’m way past due in getting it to the community. I last mentioned it here with some more detail. It’s very simple, very similar to what you are doing, and very quick. And it uses the same concept, where adding/changing/removing a device to the automation is as simple as adding it to a group. If it’s lighting depends on lux, then add lux triggers and values to metadata. If the action needs a timer, add it to metadata.

Basically, take all of the logic out of your rule and put it into groups, with Area_Triggers and Area_Actions as groups using the associated Items DP. Very similar to what you’ve done! I think we’ve actually communicated about this before.

1 Like

Sounds very interesting! I’d love to see it!

I’ve held off from using metadata because there’s no super simple way of maintaining it via the Paper UI. I base my logic around groups: room groups, a lighting group, a motion detector group, a Lux sensor group, a Lux threshold group, etc. All coordinated around the intersection with the room groups. The “member of group” trigger was a HUGE thing for reducing the amount of code and rules in my system.

Eos has an editor for its metadata. It’s a good place to start until I have the docs up.

@ysc, is metadata editing functional in the UI you are working on? If not, I’ve thought about adapting the PersistenceViewer for reading/editing metadata. It really shouldn’t take that much effort, but it’s been easy enough to use scripts for it…

from core.metadata import set_metadata, remove_metadata

set_metadata("US_GarageAttached_Dimmer", "Area_Action", {"Timer": {"OFF": {"Time": 180}}}, overwrite=True)
set_metadata("US_MasterBathroom_Speaker_Player", "Area_Action", {"Timer": {"OFF": {"Time": 30}}}, overwrite=True)
set_metadata("DS_MasterBathroom_Speaker_Player", "Area_Action", {"Timer": {"OFF": {"Time": 30}}}, overwrite=True)

set_metadata("DS_FamilyRoom_TV_LED_Color", "Area_Action", {"Mode": {"Morning": {"Low_Lux_Trigger":5, "Brightness":10, "Hue":100, "Saturation":100}, "Day": {"Low_Lux_Trigger":55, "Brightness":10, "Hue":255, "Saturation":100}, "Evening": {"Low_Lux_Trigger":90, "Brightness":10, "Hue":255, "Saturation":100}, "Night": {"Low_Lux_Trigger":90, "Brightness":10, "Hue":240, "Saturation":100}, "Late": {"Low_Lux_Trigger":5, "Brightness":10, "Hue":0, "Saturation":100}}}, overwrite=True)

set_metadata("DS_Kitchen_Sink_Switch", "Area_Action", {"Mode": {"Morning": {"Low_Lux_Trigger":5, "Brightness":98}, "Day": {"Low_Lux_Trigger":90, "Brightness":98}, "Evening": {"Low_Lux_Trigger":90, "Brightness":98}, "Night": {"Low_Lux_Trigger":90, "Brightness":98}, "Late": {"Low_Lux_Trigger":90, "Brightness":0}}}, overwrite=True)
set_metadata("DS_Kitchen_Spots_Dimmer", "Area_Action", {"Mode": {"Morning": {"Low_Lux_Trigger":5, "Brightness":98}, "Day": {"Low_Lux_Trigger":90, "Brightness":98}, "Evening": {"Low_Lux_Trigger":90, "Brightness":98}, "Night": {"Low_Lux_Trigger":90, "Brightness":98}, "Late": {"Low_Lux_Trigger":90, "Brightness":0}}}, overwrite=True)

Yep, it’s kind of functional but the goal is to ship it with OH3, not before :wink:

Thank you for the reply… completely understood! I just don’t recall seeing it last time I took a look… glad to hear it’s evolving. I’ll take another peak before considering building something on my own for manipulating metadata.

I’ve not looked really closely at this and I may have missed something, but wouldn’t a filter be appropriate here?

triggerinRoomGroup = filter(lambda roomGroup: roomGroup.type == "Group" and ir.getItem[event.itemName] in roomGroup.members, ir.getItem["gRoom"].members)

If I haven’t messed up the syntax this should replace the first set of nested loops, right?

I tried removing these lines:

for roomGroup in ir.getItem("gRoom").members:
    if roomGroup.type == "Group":
        for roomMember in ir.getItem(str(roomGroup.name)).members:
            if roomMember.name == event.itemName:
                triggeringRoomGroup = roomGroup

and replaced them with:

triggerinRoomGroup = filter(lambda roomGroup: roomGroup.type == "Group" and ir.getItem[event.itemName] in roomGroup.members, ir.getItem["gRoom"].members)

but got the following error in the log when I triggered the rule:

2019-08-12 21:06:53.701 [INFO ] [mple Channel event rule (decorators)] - gLightingPresetSetting received update
2019-08-12 21:06:53.702 [ERROR] [mple Channel event rule (decorators)] - Traceback (most recent call last):
  File "/etc/openhab2/automation/lib/python/core/log.py", line 43, in wrapper
    return fn(*args, **kwargs)
  File "<script>", line 17, in dev_lighting_preset_selection
TypeError: 'instancemethod' object is unsubscriptable

I can’t say that error message really gives me much of a clue as to what the problem is.

I’ve seen that error before and I can’t for the life of me remember what it meant. Maybe I’m wrong about roomGroup,members being a Python list of Items?

Oh, total bone headed typo on my part, use parens with ir.getItem, not brackets.

... ir.getItem(event.itemName) in roomGroup.members, ir.getItem("gRoom").members

Progress of sorts, the error message is now:

2019-08-12 21:19:12.621 [INFO ] [mple Channel event rule (decorators)] - gLightingPresetSetting received update
2019-08-12 21:19:12.622 [ERROR] [mple Channel event rule (decorators)] - Traceback (most recent call last):
  File "/etc/openhab2/automation/lib/python/core/log.py", line 43, in wrapper
    return fn(*args, **kwargs)
  File "<script>", line 18, in dev_lighting_preset_selection
AttributeError: 'NoneType' object has no attribute 'name'

2019-08-12 21:19:12.622 [ERROR] [omation.core.internal.RuleEngineImpl] - Failed to execute rule '55a142a8-29d4-4834-a4bf-48de19f3d519': Fail to execute action: 1

Hmmmmm. That error is occurring on the next line of code. I’m going to guess that the filter is returning None. That means it’s at least syntactically correct but it is not successfully finding the Item.

If you want to continue down this path debugging, the next step I’d take is to split it up a bit. Since you have a working solution it may not be worth your effort.

roomGroups = filter(lambda roomGroup: roomGroup.type == "Group", ir.getItem("gRoom").members
dev_lighting_preset_selection.log.info("Found groups: {}".format(roomGroups))

That will show what the first part of the conditional does. If that looks good, next try:

roomGroup = filter(lambda rmGrp: ir.getItem(event.itemName) in rmGrp.members, roomGroups)
dev_lighting_preset_selection.log.info("Found group: {}".format(roomGroup))

That will show if the second part returns the right Group or no Group at all.

But it occurs to me that we don’t have to go about it this way. Is there a way you can go about it the other direction by checking the names of the Groups that the triggering Item is a member of?

roomGroup = filter(lambda grp: grp == ???, ir.getItem(event.itemName).getGroupNames())

If the Item is only a member of one Group then it’s even easier, just get the first element from getGroupNames(). If there are more than one, maybe you can use an Associated Item type naming scheme along the lines of:

roomGroup = filter(lambda grp: startswith("Room"), ir.getItem(event.itemName).getGroupNames())

All of this is assuming that the List returned by the Item is a proper Python List and not a java.util.List in which case all bets are off.

Adding the first line you suggested:

roomGroups = filter(lambda roomGroup: roomGroup.type == "Group", ir.getItem("gRoom").members
    dev_lighting_preset_selection.log.info("Found groups: {}".format(roomGroups))

gives the following syntax error in the log:

2019-08-12 21:57:07.065 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab2/automation/jsr223/personal/lighting_preset.py': SyntaxError: no viable alternative at input 'dev_lig
hting_preset_selection' in <script> at line number 22 at column number 8

I’m not being much help other than reporting the errors in the logs, sorry. Since I’ve only just started working with Jython rules I don’t have much wisdom to add! I do find this very interesting though and if “we” get these filter statements working I’d be interested to see if there’s a way to evaluate the performance of the different approaches.

Now it’s complaining about the log statement. Did the name of your Rule function change since the earlier posts?

The @rule decorator automatically creates a logger for the Rule and you access the logger using the <name of the function>.log. For example, I have

@rule("Is Cloudy", description="Generates an event when it's cloudy or not", tags=["weather"])
@when("Item vCloudiness changed")
def is_cloudy(event):
    # some code

To log I would call is_cloudy.log.info("My log statement").

So either the Rule function changed names since the first posting that shows the whole code, or I have a typo in the name of the function because it is saying that dev_lighting_preset_selection doesn’t exist.

I don’t mind working through these errors with you as I think it’s a learning experience for both of us.

I suspect both versions will run so fast there will be no measurable difference.

Is my third approach idea above feasible (just search through the Groups that the triggering Item is a member of directly)? If so that would be the simplest and most efficient way to achieve this of them all.

Yes… that’s what I was hinting at :wink:.

I think this error may be due to a bad indent. The rules DSL doesn’t care about indents and dedents. This is not the case in Python… https://docs.python.org/2.0/ref/indentation.html. I suggest to us VS Code for editing your scripts and to only uses spaces instead of tabs (I think this is the default setting).

Try this…

    triggeringRoomGroup = [group for group in ir.getItem("gRoom").members if ir.getItem(itemName) in group.members][0]

This assumes there are only groups in gRoom. If there are more than GroupItems in gRoom, then…

    triggeringRoomGroup = [group for group in ir.getItem("gRoom").members if group.type == "Group" and ir.getItem(itemName) in group.members][0]

The equivalent using filter and a lambda is…

triggeringRoomGroup = filter(lambda group: group.type == "Group" and ir.getItem(itemName) in group.members, ir.getItem("gRoom").members)[0]

List and dict comprehension (search those terms) are very powerful features of Python. Lambdas can also be used, and I’d used them in the examples since DSL users would recognize them. The docs now have examples for both.

The complexity of your script would be reduced if you added your triggering Items to groups (i.e. gFamilyRoom_Trigger) which were all in a parent group (gArea_Trigger). Then your rule could trigger on changes to the members of gArea_Trigger. Your action Items, like lights, etc., would go into groups too and the triggering group would call it’s associated action group. Since your using other Items to store configuration data instead of metadata, these can stay in your regular gRoom groups and access them with associated Items.

This is a portion of my script to illustrate what I mean. You’d ask about comparing the times of certain changes, so I’ve included how I check how changes to the script effects performance. In my case, it takes ~5ms on average for the rule to run. At one point, I’d included a running averaging of the times, but took it out since it was affecting performance!

@rule("Light: Area trigger")
@when("Member of gArea_Trigger changed")
def area_trigger(event):
    start_time = DateTime.now().getMillis()
    area_trigger.log.debug("{}: {}: start".format(event.itemName, event.itemState))
    group_name = event.itemName.replace("_Trigger","")
    if "Speaker" in group_name:
        speaker_action(group_name, event)
    else:
        action_group = ir.getItem("{}_Action".format(group_name))
        for light_item in action_group.getMembers():
            ...
    area_trigger.log.critical("{}: {}: time=[{}]".format(event.itemName, event.itemState, DateTime.now().getMillis() - start_time))

That worked, well, almost, I spotted your deliberate mistake that you put there to test me. The itemName was missing the event. in front of it. The code to get the triggeringRoomGroup is now:

triggeringRoomGroup = [group for group in ir.getItem("gRoom").members if group.type == "Group" and ir.getItem(event.itemName) in group.members][0]

The nested for loop that follows it finds the lighting items in group gLighting that are also in the triggeringRoomGroup. It’s very similar to the filter expression that gets the triggering room group except we want to get all the members of gLighting that are also in the triggeringRoomGroup, not just the first member that matches (wihch is I guess the “[0]” does at the end of the above code).

I’ve put the code to set the item state to the value store in the preset item in a function:

def presetItemSendCommand(lightingItem, event):
    dev_lighting_preset_selection.log.info("test function ({})". format(event.itemName))

    lightingPresetName = "{}_gLightingPreset{}".format(lightingItem.name, event.itemState)

    dev_lighting_preset_selection.log.info("NG4: Lighting preset item name is ({}), with state ({}).".format(
        lightingPresetName, ir.getItem(str(lightingPresetName)).state))

    if ir.getItem(str(lightingPresetName)).state != NULL:
        events.sendCommand(lightingItem, items[lightingPresetName]) 
    else:
        events.sendCommand(lightingItem, OFF)

So the rule now looks like this:

@rule("Example Channel event rule (decorators)", description="This is an example rule that is triggered by the sun setting", tags=["Example", "Tag1"])
@when("Member of gLightingPresetSetting received update")
def dev_lighting_preset_selection(event):
    triggeringRoomGroup = None
    presetItem          = None
    dev_lighting_preset_selection.log.info("gLightingPresetSetting received update")

    triggeringRoomGroup = [group for group in ir.getItem("gRoom").members if group.type == "Group" and ir.getItem(event.itemName) in group.members][0]
    # presetItem          = [group for group in ir.getItem(str(triggeringRoomGroup.name)).members if ir.getItem(event.itemName) in group.members]
    dev_lighting_preset_selection.log.info("NG4: Triggering item was ({}), in room group ({}) with state ({}).".format(
        event.itemName, triggeringRoomGroup.name, event.itemState))
    for roomItem in ir.getItem(str(triggeringRoomGroup.name)).members:
        for lightingItem in ir.getItem(str("gLighting")).members:
            if roomItem.name == lightingItem.name:
                dev_lighting_preset_selection.log.info("NG4: Lighting item name is ({}), in room group ({}) with state ({}).".format(
                    lightingItem.name, triggeringRoomGroup.name, lightingItem.state))

                presetItemSendCommand(lightingItem, event)

Is it possible do a similar sort of filter expression to get the lightingItems but call presetItemSendCommand() from within that same line of code? I.e., combine the roomItem and lightingItem for loops together and call presetItemSendCommand() for every match? I’ve been reading python documentation and feel like I can almost do it but can’t quite get there…

Maybe by using map()?

I’ve been doing a bit more work on this and have the following code in my rule:

triggeringRoomGroup = [group for group in ir.getItem("gRoom").members if group.type == "Group" and ir.getItem(event.itemName) in group.members][0]
lightItem           = map( printItem, [lightItem for lightItem in ir.getItem(str("gLighting")).members if lightItem.type != "Group"])

with the function printItem being:

def printItem(item):
    dev_lighting_preset_selection.log.info("blah blah blah ({})". format(item))

This results in the following in the log:

2019-08-13 20:48:48.758 [INFO ] [mple Channel event rule (decorators)] - gLightingPresetSetting received update
2019-08-13 20:48:48.759 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (KitchenRGBWLight1_Dimmer (Type=DimmerItem, State=0, Label=Dimmer, Category=DimmableLight, Groups=[gKitchen, gLighting]))
2019-08-13 20:48:48.760 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (DimmerKitchenCeilingRing_Dimmer (Type=DimmerItem, State=0, Label=Dimmer Switch 1, Category=DimmableLight, Groups=[gKitchen, gLighting]))
2019-08-13 20:48:48.760 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (BulbDimmableWhiteTemp4_Brightness (Type=DimmerItem, State=0, Label=Brightness, Category=DimmableLight, Groups=[gTopLanding, gLighting]))
2019-08-13 20:48:48.760 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (BulbDimmableWhiteTemp2_Brightness (Type=DimmerItem, State=100, Label=Brightness, Category=DimmableLight, Groups=[gLauriesRoom, gLighting])
)
2019-08-13 20:48:48.761 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (SwitchSocket5_Switch (Type=SwitchItem, State=OFF, Label=Switch, Category=Switch, Groups=[gJulessRoom, gLighting]))
2019-08-13 20:48:48.761 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (DimmerKitchenCupboards_Dimmer (Type=DimmerItem, State=0, Label=Dimmer Switch 1, Category=DimmableLight, Groups=[gKitchen, gLighting]))
2019-08-13 20:48:48.761 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (SwitchSocket4_Switch (Type=SwitchItem, State=OFF, Label=Switch, Category=Switch, Groups=[gFrontroom, gLighting]))
2019-08-13 20:48:48.762 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (LightCeilingBlueroomDimmer_Dimmer (Type=DimmerItem, State=100, Label=Dimmer, Category=DimmableLight, Groups=[gBlueRoom, gLighting]))
2019-08-13 20:48:48.762 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (DimmerBathroomCeilingLights_Dimmer (Type=DimmerItem, State=0, Label=Dimmer, Category=DimmableLight, Groups=[gBathroom, gLighting]))
2019-08-13 20:48:48.762 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (SwitchSocket3_Switch (Type=SwitchItem, State=OFF, Label=Switch, Category=Switch, Groups=[gKitchen, gLighting]))
2019-08-13 20:48:48.763 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (DimmerMiddleLandingCeiling_Dimmer (Type=DimmerItem, State=0, Label=Dimmer 1, Category=DimmableLight, Groups=[gMiddleLanding, gLighting]))
2019-08-13 20:48:48.763 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (SwitchSocket6_Switch (Type=SwitchItem, State=OFF, Label=Switch, Category=Switch, Groups=[gBlueRoom, gLighting]))
2019-08-13 20:48:48.764 [INFO ] [mple Channel event rule (decorators)] - blah blah blah (LaurieDoorLight_Brightness (Type=DimmerItem, State=100, Label=Brightness, Category=DimmableLight, Groups=[gLauriesRoom, gLighting]))

One of the attributes of lightItem is the groups it’s in. Awesome! I then tried to incorporate a check on the lightItem.Groups list:

lightItem = map( printItem, [lightItem for lightItem in ir.getItem(str("gLighting")).members if lightItem.type != "Group" and triggeringRoomGroup.name in lightItem.Groups])

But that results in the following error in the log:

2019-08-13 20:51:59.445 [ERROR] [mple Channel event rule (decorators)] - Traceback (most recent call last):
  File "/etc/openhab2/automation/lib/python/core/log.py", line 43, in wrapper
    return fn(*args, **kwargs)
  File "<script>", line 13, in dev_lighting_preset_selection
AttributeError: 'org.eclipse.smarthome.core.library.items.DimmerIte' object has no attribute 'Groups'

2019-08-13 20:51:59.445 [ERROR] [omation.core.internal.RuleEngineImpl] - Failed to execute rule '59b4c1ab-373b-4863-a5fa-bbda6b491ace': Fail to execute action: 1

There’s no attribute called Groups? But I just saw it in the log earlier. Any ideas what I’m missing?

I think you need to call getGroupNames()

...triggeringRoomGroup.name in lightItem.getGroupNames()])