Bedside lights Tradfri hack - one remote, two actions

I now added group logic to deploy the same approach in different rooms.

Here’s what I changed:

// Group for al triple-toggle items
Group gTripleToggle

// Triple-toggle group: all living room lights
Group gTripleToggle_LivingDining (gTripleToggle)

// Triple-toggle group: all living room lights
Group gTripleToggle_MasterBedroom (gTripleToggle)

I then assigned the relevant _Toggle items to the related gTripleToggle group, as in:

Switch	GF_LivingDining_Uplighters_Light_Toggle	"Uplighters"	<light>	(GF_LivingDining, gLight, gTradfriLight, gTripleToggle_LivingDining)	{
	channel="tradfri:0100:gwabcdefabcdef:65537:brightness,tradfri:0100:gwabcdefabcdef:65551:brightness,tradfri:0100:gwabcdefabcdef:65539:brightness,tradfri:0100:gwabcdefabcdef:65540:brightness"
}

Now the rules read as follows:

// import org.eclipse.smarthome.model.script.ScriptServiceUtil
import java.util.HashMap

val HashMap<String, Timer> timers = newHashMap

val HashMap<String, Integer> timer_counts = newHashMap

// Will contain the state at 1st toggle (will be the end state)
val HashMap<String, String> timer_states = newHashMap

// Will contain the gTripleToggle group
val HashMap<String, GroupItem> gTripleToggle_groupItems = newHashMap

var Timer timer_Tradfri_startup = null

var boolean STARTED_UP = false

rule "System startup - Tradfri TripleToggle"
when
	System started
then
    val String logTitle = "System startup - Tradfri TripleToggle"
    val int STARTUP_DELAY_SECONDS = 10 // 30
	logInfo(logTitle, "Initializing timers after {} seconds", STARTUP_DELAY_SECONDS)

    STARTED_UP = false;
    if (timer_Tradfri_startup === null) {
        // Wait 5 seconds before resetting all timers
        timer_Tradfri_startup = createTimer(now.plusSeconds(STARTUP_DELAY_SECONDS), [ |
            STARTED_UP = true;

            // Iterate over all group toggle items (by resolving the groups)
            gTripleToggle.allMembers.forEach[ GenericItem toggleItem |
                logInfo("gTripleToggle", "Checking group membership of TripleToggle item {}", toggleItem.name)
                // For each item, iterate over the TripleToggle groups and check if the item belongs to one of these toggle groups
                gTripleToggle.members.filter[ s | s instanceof GroupItem ].forEach[ GroupItem toggleGroup |
                    logDebug("gTripleToggle", "Checking if item {} belongs to TripleToggle Group {}", toggleItem.name, toggleGroup.name)
                    if ( toggleItem.getGroupNames.contains(toggleGroup.name) ) {
                        logInfo("gTripleToggle", "MACTH: item {} belongs to TripleToggle Group {}", toggleItem.name, toggleGroup.name)
                        gTripleToggle_groupItems.put(toggleItem.name, toggleGroup)
                    }
                ]
                // Now initialize the timers
                if (timers.get(toggleItem.name) !== null) {
                    timers.put(toggleItem.name, null)
                }
            ]
            timer_Tradfri_startup = null;
    	logInfo(logTitle, "Timers initialized")
        ])
    } else {
        timer_Tradfri_startup.reschedule(now.plusSeconds(STARTUP_DELAY_SECONDS))
    }
end

rule "Member of gTripleToggle changed"
when
    /* DOES NOT WORK (probably because it doesn't resolve subgroups):
    Member of gTripleToggle changed
    */
    Member of gTripleToggle_MasterBedroom changed
    or Member of gTripleToggle_LivingDining changed
then
    val String logTitle = "Member of gTripleToggle changed"
    val int MONITORING_TIME_SECONDS = 3 // Will reset each time a click has been received, so it is okay to keep it short
    
    if (STARTED_UP !== true) {
        logWarn(logTitle, "Not yet initialized - nothing to do")
    } else {
        // Started up - rule items have been initialized

        if (triggeringItem === null) {
            logWarn(logTitle, "TripleToggle - AT START OF RULE - TriggeringItem is null (after OH restart?) - nothing to do",
                name, tripleToggleGroup.name, triggeringItem.state)
        } else {
            val String name = triggeringItem.name
            val GroupItem tripleToggleGroup = gTripleToggle_groupItems.get(name)
            if ( (triggeringItem.state == NULL) || (triggeringItem.state == UNDEF) ) {
                logWarn(logTitle, "TripleToggle - AT START OF RULE - {} (in group {}) has state {} - nothing to do",
                    name, tripleToggleGroup.name, triggeringItem.state)
            } else {
                logDebug(logTitle, "TripleToggle - AT START OF RULE - {} (in group {}) has state {} - starting the logic",
                    name, tripleToggleGroup.name, triggeringItem.state)

                if (timers.get(name) === null) {
                    // No timer running - start a timer:
                    logDebug(logTitle, "TripleToggle {} has state {} (will be the end state) - monitoring started for {} seconds", name, triggeringItem.state, MONITORING_TIME_SECONDS)
                    timers.put(name, createTimer(now.plusSeconds(MONITORING_TIME_SECONDS), [ |
                        logDebug(logTitle, "TripleToggle {} - monitoring ended after {} seconds of inactivity", name, MONITORING_TIME_SECONDS)
                        // Reset the counter and kill the timer when the timer expires
                        timer_counts.put(name, 0)
                        //timer_toggling.put(name, false)
                        timers.put(name, null)
                    ]))
                    // Set the toggle count to 1
                    timer_counts.put(name, 1)
                    // Store the desired end state (current state of triggeringItem)
                    timer_states.put(name, triggeringItem.state.toString)
                } else {
                    val int cnt = timer_counts.get(name) + 1
                    val String stateInfo = timer_states.get(name)
                    if (cnt >= 3) {
                        logInfo(logTitle, "TripleToggle {} (end state will be {}) toggle count: {} ≥ 3 -- Switching {} both bedside lights",
                            name, stateInfo, cnt, stateInfo)

                        gTripleToggle_groupItems.get(name).allMembers.forEach[s |
                            // Send the command
                            s.sendCommand(stateInfo)
                            // Cancel any running timer
                            timers.get(s.name)?.cancel
                            timers.put(s.name, null)
                        ]
                    } else {
                        logDebug(logTitle, "{} (end state will be {}) toggle count: {} - rescheduling the counter (adding {} seconds)",
                            name, stateInfo, cnt, MONITORING_TIME_SECONDS)
                        timers.get(name).reschedule(now.plusSeconds(MONITORING_TIME_SECONDS))
                        timer_counts.put(name, cnt)
                    }
                }
            }
        }
    }
end

I was surprised why

    Member of gTripleToggle changed

does not work. I could however understand that it relates to the groupItem.members() versus groupItem.allMembers() logic, but it’s a bit odd that in rules, groups are not resolved to the ‘leaves’ (the actual items, irrespective of group nesting).