How to find out which group item caused a "group rule" to trigger

Tags: #<Tag:0x00007f5c96de1a58> #<Tag:0x00007f5c96de18f0> #<Tag:0x00007f5c96de17d8>

I am trying to write a rule to turn on/off my home cinema, when one of the two devices that uses it turns ON (e.g. Chromecast or HTPC). Additionally the rule should switch the AVR to the corresponding input - even if one of the devices is already turned on.
Conversely, another rule shuts down the cinema only when, none of these devices is turned on anymore.

I have come up with a setup, where I have the two devices in an OR-group (see below). So if one of them turns on, the group will turn on and trigger the rule to turn on the cinema (and vice-versa for turning it off).
I am now struggling with the part of setting the input of the AVR. For this I somehow need to find out from inside the rule, which device of the group caused the group to change and hence trigger the rule.
I do not know how to achieve this. I have googled a bit, but was unable to come up with a solution.
I found this post that mentions triggeringItem.name, which seems like what I need and should at some point be introduced for groups too. However, I am unable to figure out if this is already possible for groups and how to use it (seems to me this only works with DSL rules?!).

Additionally, I have a question regarding the rule condition for switching inputs, when the group is already ON:

Right now I am using this:

@when("Item Home_Cinema changed from OFF to ON")                                                                                                                                                                   

I am guessing I will have to change this to something like:

@when("Item Home_Cinema updated")                                                                                                                                                                   

This should trigger the rule, when the group is already on and I turn on the second device, so that the rule switches to this device. The would be that the group gets updated to the same value (ON) in this case.
Is this possible?

It seems to me that all of this logic needs to go into a single rule, because of the grouping and because the switching of inputs will only work once the AVR is turned on. But perhaps there is a much cleaner/easier way to achieve this?! If so, I am open for suggestions.

Items:

Group:Switch:OR(ON,OFF)  Home_Cinema       "Devices using the home cinema."

Switch Chromecast_Livingroom_Idle        "Chromecast Livingroom Idle"                     <network> (gAssistent) { channel="chromecast:chromecast:livingroom:idling" }
Switch pChromecast_Livingroom_Active     "Chromecast Livingroom Active"    <network>                (Home_Cinema)  // THIS IS PROXY SWITCH THAT INVERTS Chromecast_Livingroom_Idle
Switch HTPC (Home_Cinema) { channel="network:pingdevice:htpc:online" }

Rule for turning the cinema on:

@rule("Turn on home cinema", description="Turn ON the home cinema", tags=["home_cinema"])                                                                                                                          
@when("Item Home_Cinema changed from OFF to ON")                                                                                                                                                                   
def turn_on(event):                                                                                                                                                                                                
    turn_on.log.info("Turned ON home cinema.")                                                                                                                                                                     
    turn_on.log.info("Triggering item:" + event.triggeringItem.toString)                                                                                                                                           
    sendCommandCheckFirst("projector_screen", "ON")                                                                                                                                                                
    sendCommandCheckFirst("denon_x3500h_power","ON")                                                                                                                                                               
    if(event.itemName == "pChromecast_Livingroom_Active"):                                                                                                                                                         
        sendCommandCheckFirst("denon_x3500h_input","MPLAY")                                                                                                                                                        
    if(event.itemName == "HTPC"):                                                                                                                                                                               
        sendCommandCheckFirst("denon_x3500h_input","GAME")

Rule for turning the cinema off:

@rule("Turn off home cinema", description="Turn OFF the home cinema", tags=["home_cinema"])                                                                                                                        
@when("Item Home_Cinema changed from ON to OFF")                                                                                                                                                                   
def turn_off(event):                                                                                                                                                                                               
    turn_off.log.info("Turned OFF home cinema.")                                                                                                                                                                   
    turn_on.log.info("Triggering item:" + event.triggeringItem.toString)                                                                                                                                           
    sendCommandCheckFirst("projector_screen", "OFF")                                                                                                                                                              
                                                                                                                                                                                                                   
    avr_state = getItemValue("denon_x3500h_input","ERROR")
    if(avr_state == "ERROR"):
        turn_off.log.error("denon_x3500h_input: could not get item value.")

    if(avr_state == "MPLAY" or avr_state == "GAME"):
        sendCommandCheckFirst("denon_x3500h_power","OFF")
        pass
    else:
        turn_off.log.info("getItemValue('denon_x3500h_input') != 'MPLAY': Not turning off 'denon_x3500h_power'")                                                                                                  

It’s like you want a “member of Group” trigger

triggeringItem only exists in the DSL, but scripted automation has event.itemName, event.itemState, etc… https://openhab-scripters.github.io/openhab-helper-libraries/Guides/But%20How%20Do%20I.html#get-the-triggering-item-s-name.

Don’t trigger the rule when the group is updated (groups don’t receive updates)… use a “Member of” trigger instead. I also combined both rules into one…

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

@rule("Turn ON/OFF the home cinema", description="Turn ON/OFF the home cinema", tags=["home_cinema"])
@when("Member of Home_Cinema received update")
def turn_on_off_cinema(event):
    if isinstance(items[event.itemName], UnDefType): # stop the rule if the state is NULL or UNDEF
    return
    
    turn_on_off_cinema.log.info("Turned {} home cinema".format(event.itemState))
    
    sendCommandCheckFirst("projector_screen", event.itemState.toString())

    if event.itemState == ON:
        sendCommandCheckFirst("denon_x3500h_power", "ON")
    elif event.itemState == OFF:
        avr_state = getItemValue("denon_x3500h_input","ERROR")
        if avr_state == "ERROR":
            turn_off.log.error("denon_x3500h_input: could not get item value.")
        elif avr_state == "MPLAY" or avr_state == "GAME":
            sendCommandCheckFirst("denon_x3500h_power","OFF")
        else:
            turn_off.log.info("getItemValue('denon_x3500h_input') != 'MPLAY': Not turning off 'denon_x3500h_power'")

    if event.itemName == "pChromecast_Livingroom_Active":
        sendCommandCheckFirst("denon_x3500h_input", "MPLAY")
    elif event.itemName == "HTPC":
        sendCommandCheckFirst("denon_x3500h_input", "GAME")

This is great! “Member of” trigger is what I need. However I do have one more question:

In you example, it seems that event.itemName, will hold the name of the group member that triggered the event. So it will not do exactly what I need, because it will change the state of the home cinema (of projector_screen and denon_x3500h_power) to the state of that group member (if I understand correctly). Whereas what I want is to change it to the state of the group itself.

I could now easily get the group state by using:

group_state = getItemValue("Home_Cinema","ERROR")

Is this the way to go or is it possible to get the respective group of the member through a function call on the event/rule? This would make it more generic, if I need to do something similar in the future …

Uh, but you know which Group you are dealing with because you hard-coded it into the rule trigger.

You can find out which Groups a random Item belongs to … but it may belong to many.

Exactly, but I do not know how to get the group from inside the rule. If I understand @5iver’s example correctly, then event.itemName will give me the member of the group (which I also want), but not the group itself. And I need both.

Given your examples, you already know the name of the Group. You’ve only one trigger using only one Group, Home_Cinema. You don’t need to figure anything out, just use “Home_Cinema”.

However, if you have more than one Member of trigger on the Rule, as rossko57 indicates, you can only determine which of the Groups the Item that triggered the Rule if none of the Items belong to both Groups. If that’s the case, as he indicated, you can see which of the Groups event.itemName belongs to. If I recall correctly it would be something like:

if "Home_Cinema" in ir.getItem(event.itemName).getGroupNames():

Did this change? It used to be the case that a Group with a type, and especially a Group with a type and an aggregation function would receive lots and lots of updates (n-1 updates where n is the number of members of the Group) when ever one of it’s members updated. This behavior was how we used to figure out which member of a Group triggered a Rule before the Member of triggers were developed.

For anyone interested in my final rule. This is what I have ATM thanks to @5iver:

from core.rules import rule
from core.triggers import when
from core.utils import sendCommandCheckFirst, getItemValue

@rule("Turn ON/OFF the home cinema", description="Turn ON/OFF the home cinema", tags=["home_cinema"])
@when("Member of Home_Cinema received update")
def turn_on_off_cinema(event):
    if isinstance(items[event.itemName], UnDefType): # stop the rule if the state is NULL or UNDEF
        return
    
    group_state = ir.getItem("Home_Cinema").state

    turn_on_off_cinema.log.info("Turned {} home cinema".format(event.itemState))
    
    sendCommandCheckFirst("projector_screen", group_state)

    if group_state == ON:
        sendCommandCheckFirst("denon_x3500h_power", ON)

        chromecast = ir.getItem("pChromecast_Livingroom_Active")
        htpc = ir.getItem("HTPC")
        trigger_item = ir.getItem(event.itemName)

        if trigger_item == chromecast and chromecast.state == ON: # triggered by chromecast, so switch to it
            sendCommandCheckFirst("denon_x3500h_input", "MPLAY")
            turn_on_off_cinema.log.info("Switched denon_x3500h_input to 'MPLAY'")
        elif trigger_item == htpc and htpc.state == ON: # triggered by htpc, so switch to it
            sendCommandCheckFirst("denon_x3500h_input", "GAME")
            turn_on_off_cinema.log.info("Switched denon_x3500h_input to 'GAME'")
        elif trigger_item == htpc and htpc.state == OFF and chromecast.state == ON: # switch back to chromecast, if ON
            sendCommandCheckFirst("denon_x3500h_input", "MPLAY")
            turn_on_off_cinema.log.info("Switched denon_x3500h_input to 'MPLAY'")
        elif trigger_item == chromecast and chromecast.state == OFF and htpc.state == ON: # switch back to htpc, if ON
            sendCommandCheckFirst("denon_x3500h_input", "GAME")

    elif group_state == OFF:
        avr_state = getItemValue("denon_x3500h_input","ERROR")
        if avr_state == "ERROR":
            turn_off.log.error("denon_x3500h_input: could not get item value.")
        elif avr_state == "MPLAY" or avr_state == "GAME": # turn of AVR only, if input is not htpc or chromecast
            sendCommandCheckFirst("denon_x3500h_power",OFF)
        else:
            turn_off.log.info("denon_x3500h_input != 'MPLAY' or 'GAME': Not turning off 'denon_x3500h_power'")
1 Like

It’s easier to use `items for this and then there’s no need to set a variable for an Item’s state…

if items["Home_Cinema"] == ON:
    ...
elif items["Home_Cinema"] == OFF:
    ...

I missed this before, but this function has been renamed to send_command_if_different. What you have will work for now, but that may change in the future.

For several reasons, it is better to compare Item names than the Item objects themselves…

if event.itemName == "pChromecast_Livingroom_Active" and items["pChromecast_Livingroom_Active"] == ON:

So, something like this…

from core.rules import rule
from core.triggers import when
from core.utils import send_command_if_different, getItemValue

@rule("Turn ON/OFF the home cinema", description="Turn ON/OFF the home cinema", tags=["home_cinema"])
@when("Member of Home_Cinema received update")
def turn_on_off_cinema(event):
    if isinstance(items[event.itemName], UnDefType): # stop the rule if the state is NULL or UNDEF
        return
    
    turn_on_off_cinema.log.info("Turned {} home cinema".format(event.itemState))

    send_command_if_different("projector_screen", items["Home_Cinema"])

    if items["Home_Cinema"] == ON:
        send_command_if_different("denon_x3500h_power", ON)

        if event.itemName == "pChromecast_Livingroom_Active":
            if items["pChromecast_Livingroom_Active"] == ON:# triggered by chromecast, so switch to it
                send_command_if_different("denon_x3500h_input", "MPLAY")
                turn_on_off_cinema.log.info("Switched denon_x3500h_input to 'MPLAY'")
            elif items["pChromecast_Livingroom_Active"] == OFF and items["HTPC"] == ON:# switch back to htpc, if ON
                send_command_if_different("denon_x3500h_input", "GAME")
        elif event.itemName == "HTPC":
            if items["HTPC"] == ON:# triggered by htpc, so switch to it
                send_command_if_different("denon_x3500h_input", "GAME")
                turn_on_off_cinema.log.info("Switched denon_x3500h_input to 'GAME'")
            elif items["HTPC"] == OFF and items["pChromecast_Livingroom_Active"] == ON:# switch back to chromecast, if ON
                send_command_if_different("denon_x3500h_input", "MPLAY")
                turn_on_off_cinema.log.info("Switched denon_x3500h_input to 'MPLAY'")

    elif items["Home_Cinema"] == OFF:
        avr_state = getItemValue("denon_x3500h_input", "ERROR")
        if avr_state == "ERROR":
            turn_on_off_cinema.log.error("denon_x3500h_input: could not get item value.")
        elif avr_state in ["MPLAY", "GAME"]:# turn of AVR only, if input is not htpc or chromecast
            send_command_if_different("denon_x3500h_power", OFF)
        else:
            turn_on_off_cinema.log.info("denon_x3500h_input != 'MPLAY' or 'GAME': Not turning off 'denon_x3500h_power'")

Thanks for these remarks. They are much appreciated. It triggers a question however:

The reason I used the item objects themselves was to avoid ‘magic strings’, so that the IDE picks up these, when I e.g. rename variables in the rule. If this is not good, then I wonder how to avoid this. Is there a best practice/design-pattern for this? I guess I could define the string inside a name variable, e.g. something like chromecast_name.

Also in this same vein: What is the best method to rename items in general? Do you do a sed /oldname/newname/ on all files in $OPENHAB_CONF? That would seem dangerous to me, although I do have $OPENHAB_CONF und der Git supervision.

I’m not sure what you mean. There’s nothing wrong with using variables, especially if they improve the readability, but make the variable a string representation of the Item’s name, state, etc. and not the Item itself.

Ok. Got it. Thanks for your kind support!

1 Like