Help converting group based xtend rule to JSR233 please

Hi all,

I’m finally going to take the plunge and start converting my xtend rules to JSR233. I’d like to start with a lighting preset rule that relies heavily on groups but I’m not clear on how to use Jython to work with groups. The rule is:

rule "Dev Lighting Preset Selection"
when
	Member of gLightingPresetSetting received update
then
	var	GroupItem 	triggeringRoomGroup

	logInfo("dev", "NG3: Lighting preset triggered.")
	logInfo("dev", "NG3: Lighting preset triggeringItem = (" + triggeringItem.name + ") " + "has state (" + triggeringItem.state + ")." )

    // gRoomGroup = "gRoom" as GroupItem
	gRoom.members.forEach[roomGroup|
	 	if (roomGroup instanceof GroupItem)
	 	{
	 		roomGroup.members.forEach[roomMember|
	 			if (roomMember.name == triggeringItem.name)
	 			{
					triggeringRoomGroup = roomGroup
	 				logInfo("dev", "NG3: Triggering item was (" + triggeringItem.name + "), in room group (" + triggeringRoomGroup.name + ")")
	 			}
			]
	 	}]

	// Loop through all items in the room group to get items in room with required values, e.g. motion lighting timeout, lighting luminance threshold, etc
	triggeringRoomGroup.members.forEach[roomItem|

		gLighting.members.forEach[lightingMember|
			if (roomItem.name == lightingMember.name)
			{
				val lightingPresetName = lightingMember.name + "_gLightingPreset" + triggeringItem.state
				val lightingPresetItemName = gLightingPreset.members.findFirst[ i | i.name == lightingPresetName ]
				logInfo("dev", "NG3: Lighting item name is (" + lightingMember.name + ") in room group (" + triggeringRoomGroup.name + ") with state (" + lightingMember.state + ").")
				logInfo("dev", "NG3: Lighting preset item name is (" + lightingPresetItemName.name + ") with state (" + lightingPresetItemName.state + ").")
				if (lightingPresetItemName.state != NULL) {
					// logInfo("dev", "NG3: setting lighting item to preset value...")
					lightingMember.sendCommand(lightingPresetItemName.state)
					// logInfo("dev", "NG3: set lighting item to preset value.")
				}
				else {
					lightingMember.sendCommand(OFF)
				}
 				// Thread::sleep(100)
			}
		]
    ]

end

I like this rule because it allows me to move lights between rooms (I sometimes do this with Hue bulbs) and all I need to do is change the room group that the lights are in and that moves them between room presets.

Could someone point me at some examples of working with groups in Jython please? I feel like my google skills are lacking today and I must be missing something obvious.

Thanks!

You will find plenty of information for Python for loops and also list or dict comprehension. In the case of this rule you are better with the for loop though. There is also a section on Groups in the Helper Libraries documentation with some good examples that apply to openHAB directly.

I’ve converted your first group iterator to get you started (haven’t tested this):

def dev_lighting_preset_selection(event):
    triggeringRoomGroup = None

    for roomGroup in ir.getItem("gRoom").members:
        if triggeringRoomGroup: break # exits the roomGroup loop if we found the triggering group
        if roomGroup.type == "Group":
            for roomMember in roomGroup.members:
                if roomMember.name == event.itemName:
                    triggeringRoomGroup = roomGroup
                    dev_lighting_preset_selection.log.info("NG3: Triggering item was ({}), in room group ({})".format(
                            event.itemName, triggeringRoomGroup.name))
                    break # exits the roomMember loop, no need to continue if we found the triggering item

Thanks for your help, those docs are very useful. I’m failing at the first hurdle though and can’t seem to iterate through group members. I’ve got the trigger working for a specific motion detector:

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

@rule("Example Channel event rule (decorators)", description="This is an example rule", tags=["Example", "Tag1"])
@when("Item PIRMotionSensor2_MotionAlarm received update")
def dev_lighting_preset_selection(event):
    # roomGroup           = groupItem
    triggeringRoomGroup = None
    dev_lighting_preset_selection.log.info("gLightingPresetSetting received update")

    for item in roomGroup.getItem("gRoom").members:
        dev_lighting_preset_selection.log.info("NG3: Triggering item was ({}), in room group ({})".format(
            event.itemName, item.name))

When the rule is triggered the output in the log is:

2019-08-11 17:38:02.046 [ERROR] [omation.core.internal.RuleEngineImpl] - Failed to execute rule '7c9ab6eb-6b72-4585-8077-3e193a5f9bc1': Fail to execute action: 1
2019-08-11 17:38:33.155 [INFO ] [mple Channel event rule (decorators)] - gLightingPresetSetting received update 2
2019-08-11 17:38:33.156 [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 12, in dev_lighting_preset_selection
NameError: global name 'roomGroup' is not defined

Do I need to declare roomGroup as a variable with type Group first? If so, what’s the name of the Group type? I’ve tried various variations of groupItem, GroupItem, Group, etc but none work and I can’t seem to find a list of elementary types in the docs.

I’m running OH 2.4, not a the latest snapshot, in case that has any bearing what syntax I can use.

You need to getItem on the Item Registry:

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

I should have done that in my example, it was early in the morning sorry lol

A-ha! So that’s what ir is! The Item Registry object with all it’s associated methods. Thank you again :slight_smile:

My 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")
@when("Item PIRMotionSensor2_MotionAlarm received update")
def dev_lighting_preset_selection(event):
    triggeringRoomGroup = None
    dev_lighting_preset_selection.log.info("gLightingPresetSetting received update 2")

    for roomGroup in ir.getItem("gRoom").members:
        if isinstance(roomGroup, GroupItem):
            dev_lighting_preset_selection.log.info("NG3: Triggering item was ({}), in room group ({})".format(
                event.itemName, item.name))

Bue the syntax to check if an item is a group item is incorrect. The output in the log when the rule is triggered is:

2019-08-11 18:05:30.301 [ERROR] [omation.core.internal.RuleEngineImpl] - Failed to execute rule '8ca214c8-522b-47c1-8da1-e40a66aec8b5': Fail to execute action: 1
2019-08-11 18:05:57.478 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Dev Motion Detected Trigger Virtual Switch': cannot invoke method public java.lang.String org.eclipse.smarthome.core.items.GenericItem.getName() on
null
2019-08-11 18:06:24.711 [INFO ] [mple Channel event rule (decorators)] - gLightingPresetSetting received update 2
2019-08-11 18:06:24.712 [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 12, in dev_lighting_preset_selection
NameError: global name 'GroupItem' is not defined

I’ve just been looking through the docs and googling again but can’t find a list of elementary types. What’s the correct syntax for the group item type?

Boy I should have waited to wake up some before posting that haha.

You could import GroupItem but it would be easier to do this:

        if roomGroup.type == "Group":

Excellent! Thanks again for more help. I’ve made more progress and my rule now looks like this:

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

@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")
# @when("Item PIRMotionSensor2_MotionAlarm received update")
def dev_lighting_preset_selection(event):
    triggeringRoomGroup = None
    dev_lighting_preset_selection.log.info("gLightingPresetSetting received update")

    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
                    dev_lighting_preset_selection.log.info("NG4: Triggering item was ({}), in room group ({})".format(
                        event.itemName, triggeringRoomGroup.name))
                    
                    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))

                                lightingPresetName = lightingItem.name + "_gLightingPreset" + ir.getItem(event.itemName).state
                                # break # exits the roomMember loop, no need to continue if we found the triggering item

I can’t get the syntax right for the “ir.getItem(event.itemName).state” on the second to last line. I need to get the state of the triggering item. I’ve tried all sorts of incantations but can’t find the right one.

Ignore my last message. I’ve solved it! :slight_smile:

What was the issue?
And why all of the casting to string str()?

The issue was me being dumb I think. I’ve got the rule working, it currently looks like this:

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

@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
    dev_lighting_preset_selection.log.info("gLightingPresetSetting received update")

    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
                    dev_lighting_preset_selection.log.info("NG4: Triggering item was ({}), in room group ({})".format(
                        event.itemName, triggeringRoomGroup.name))
                    
                    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))

                                lightingPresetName = '%s%s%s' % (str(lightingItem.name), str("_gLightingPreset"), str(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, ir.getItem(str(lightingPresetName)).state)
                                else:
                                    lightingItem.sendCommand(OFF)
                                  
        # break # exits the roomMember loop, no need to continue if we found the triggering item

It seemed that I needed to cast to string to avoid Unicode errors but they might not be needed and I might be going overboard. I need to spend some time trying to strip some of those casts out. I’d also like to see if I can avoid some of those nested loops, maybe with an early break and possibly using some metadata to identify items as lighting items.

Thanks for all your help and patience :slight_smile:

Glad you got it working, I think I see what your issue was. You should use string format instead:

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

It attempts to cast all arguments given to a string automatically, and frankly it looks nicer. I’ve given a second example of the same thing using named substitutions, though not necessary in this case it can be useful to keep things clear or when you need the same thing repeated in the string.

In terms of metadata and what you are doing with your lights, I might have just the thing. I am nearing completion on I have written an extension for the Helper Libraries that will basically do what I think you are doing with your lights. It puts all settings in metadata and uses “scenes” like you seem to use “presets”. It is usable right now, but lets say it is in “beta”. I will be pushing updates and possibly adding a few things before it gets merged. The catch is that I haven’t written any documentation yet, but I am working on that now. If you’re feeling adventurous you can look into it now, or wait for the docs.
Eos Lighting

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.