Consolidation help

I understand from a post by Rich that we will hopefully see item Groups as a thing for consolidation going forward, this is indeed good news.

I would like to know of better ways to consolidate the rule below into a ideally shorter lines of code and improved performance.

The rules is quite simple, disabling one vSwitch disables two other vSwitches and the same goes in reverse when you enable the primary vSwitch. a parent/Children switching relationship.

I previously set up a rule for every room (BR1,BR2,BR3,BR4,LR,GR.HO) and used IF statements this made for easy to read code but a larger amount of it; the rule below decreased the number of lines in total but seems to be very repetitive, I am hoping the guru’s here can advise of even better ways for this rule to evolve.

rule "Automated Temperature Control"
when
Item BR1_Climate_auto received command or
Item BR2_Climate_auto received command or
Item BR3_Climate_auto received command or
Item BR4_Climate_auto received command or
Item LR_Climate_auto received command or
Item GR_Climate_auto received command or
Item HO_Climate_auto received command
then
	logDebug("autoclimate.rules", triggeringItem.name + " received a command: " +  receivedCommand)
	switch receivedCommand {
		case receivedCommand == ON :	{
			switch triggeringItem.name {
				case "BR1_Climate_auto" : {
					BR1_Temp_auto.sendCommand(ON)
					BR1_Humid_auto.sendCommand(ON)					
				}
				case "BR2_Climate_auto" : {
					BR2_Temp_auto.sendCommand(ON)
					BR2_Humid_auto.sendCommand(ON)					
				}
				case "BR3_Climate_auto" : {
					BR3_Temp_auto.sendCommand(ON)
					BR3_Humid_auto.sendCommand(ON)					
				}
				case "BR4_Climate_auto" : {
					BR4_Temp_auto.sendCommand(ON)
					BR4_Humid_auto.sendCommand(ON)					
				}
				case "LR_Climate_auto" : {
					LR_Temp_auto.sendCommand(ON)
					LR_Humid_auto.sendCommand(ON)					
				}	
				case "GR_Climate_auto" : {
					GR_Temp_auto.sendCommand(ON)
					GR_Humid_auto.sendCommand(ON)					
				}	
				case "HO_Climate_auto" : {
					HO_Temp_auto.sendCommand(ON)
					HO_Humid_auto.sendCommand(ON)					
				}
				default : {logError("autoclimate.rules", "Unexpectedly reached default case")}					
			}
			logInfo("autoclimate.rules", triggeringItem.name + " Climate contorl has been enabled")
		}
		case receivedCommand == OFF : {
			switch triggeringItem.name {
				case "BR1_Climate_auto" : {
					BR1_Temp_auto.sendCommand(OFF)
					BR1_Humid_auto.sendCommand(OFF)					
				}
				case "BR2_Climate_auto" : {
					BR2_Temp_auto.sendCommand(OFF)
					BR2_Humid_auto.sendCommand(OFF)					
				}
				case "BR3_Climate_auto" : {
					BR3_Temp_auto.sendCommand(OFF)
					BR3_Humid_auto.sendCommand(OFF)					
				}
				case "BR4_Climate_auto" : {
					BR4_Temp_auto.sendCommand(OFF)
					BR4_Humid_auto.sendCommand(OFF)					
				}
				case "LR_Climate_auto" : {
					LR_Temp_auto.sendCommand(OFF)
					LR_Humid_auto.sendCommand(OFF)					
				}	
				case "GR_Climate_auto" : {
					GR_Temp_auto.sendCommand(OFF)
					GR_Humid_auto.sendCommand(OFF)					
				}	
				case "HO_Climate_auto" : {
					HO_Temp_auto.sendCommand(OFF)
					HO_Humid_auto.sendCommand(OFF)					
				}
				default : {logError("autoclimate.rules", "Unexpectedly reached default case")}					
			}		
			logInfo("autoclimate.rules", triggeringItem.name + " Climate control has been disabled")
		}		
		default : {logError("autoclimate.rules", "Unexpectedly reached default case")}	
		}
end

I haven’t tested it, but this should work…

rule "Automated Temperature Control"
when
    Item BR1_Climate_auto received command or
    Item BR2_Climate_auto received command or
    Item BR3_Climate_auto received command or
    Item BR4_Climate_auto received command or
    Item LR_Climate_auto received command or
    Item GR_Climate_auto received command or
    Item HO_Climate_auto received command
then
    logDebug("autoclimate.rules", triggeringItem.name + " received a command: " +  receivedCommand)
    val sourceRoom = triggeringItem.name.split("_").get(0)
    sendCommand(sourceRoom + "_Temp_Auto",receivedCommand.toString)
    sendCommand(sourceRoom + "_Humid_Auto",receivedCommand.toString)
    logInfo("autoclimate.rules","{} Climate control has been {}",triggeringItem.name,if (receivedCommand == ON) "enabled" else "disabled")
end

As you mentioned, when you get to OH 2.3 (or if you use a snapshot build), you can add the items you are using in the triggers into a group (gClimateAuto) and then use this for the trigger…

Member of gClimateAuto received command
4 Likes

Thats is amazing!
I have tried it and with a tiniest of tweaks it works ( _Auto should be _auto ).

Not only does it cut down the code linage dramatically it is super fast too.

Thank you, I will use the same techniques on many of my rules :smile:

Thanks

Paul

1 Like

@5iver’s approach is exactly what I would have suggested.

I wrote up a DP that covers using a naming scheme like this to access related Items: Design Pattern: Associated Items

However, I’ve seen your original approach used by many before so I wanted to address a way of thinking about coding that can save you some complexity in the future even when tricks like Associated Items are not available.

So let’s work on the rule as written without that but just restructure what is written and see what we can do.

I like to apply Don’t Repeat Yourself (DRY) not only across rules but within Rules as well. I suspect that is the source of your discomfort with what you have in the OP. One way to apply DRY within a Rule is to only call Actions or methods that cause side effects (e.g. sendNotification, postUpdate, sendCommand, etc) once in the Rule. This will often require restructuring your Rule along the lines of:

  1. first determine if something needs to be done at all
  2. then calculate the new states or messages or whatever side effects need to be performed
  3. finally perform the side effects.

Looking at the original rule I see the overall task is to send an ON or OFF command to two other Items based on a received command. So what I would do is:

  • Since the Rule is triggered by a command we know that we have to do something in this Rule every time so 1. is already done.

  • The command that gets sent to the other Items is the same as receivedCommand so we will just use receivedCommand later in the rule. So the next part of 2. is to determine what Items to send the command to. there are lots of approaches including continuing to use the switch statement and assigning the Items to call to variables. There are other approaches I can think of but they all lead to Scott’s approach so I won’t go down that path right now.

  • Finally, we do the side effects which are in this case a sendCommand.

rule "Automated Temperature Control"
when
Item BR1_Climate_auto received command or
Item BR2_Climate_auto received command or
Item BR3_Climate_auto received command or
Item BR4_Climate_auto received command or
Item LR_Climate_auto received command or
Item GR_Climate_auto received command or
Item HO_Climate_auto received command
then
    logDebug("autoclimate.rules", triggeringItem.name + " received a command: " +  receivedCommand)

    // 1. decide if we need to do something at all, in this case we always 
    // want to do something so nothing needs to be done here

    // 2. Calculate what states need to change and what they need to change to
    var temp = null
    var humid = null
    switch triggeringItem.name {
        case "BR1_Climate_auto" : { temp = BR1_Temp_auto humid = BR1_Humid_auto }
        case "BR2_Climate_auto" : { temp = BR2_Temp_auto humid = BR2_Humid_auto }
        case "BR3_Climate_auto" : { temp = BR3_Temp_auto humid = BR3_Humid_auto }
        case "BR4_Climate_auto" : { temp = BR4_Temp_auto humid = BR4_Humid_auto }
        case "LR_Climate_auto" : { temp = LR_Temp_auto humid = LR_Humid_auto }
        case "GR_Climate_auto" : { temp = GR_Temp_auto humid = GR_Humid_auto }
        case "HO_Climate_auto" : { temp = HO_Temp_auto humid = HO_Humid_auto }
    }

    if(temp === null || humid === null) {
        logError("autoclimate.rules", "Unexpectedly reached default case")
        return;
    }

    // 3. Perform the actions that have side effects
    temp.sendCommand(receivedCommand)
    humid.sendCommand(receivedCommand)

    logInfo("autoclimate.rules", triggeringItem.name + " Climate control has been disabled")
end

Besides being somewhat shorter, one reason that the above is better is that should you decide that you need to do something else before sending the command or provide some additional checking or the like before sending the command you only have to do it once and in one place in the Rule. That switch statement is still kind of long and ugly (hence Scott’s solution truly being the correct one) and I would look for ways to improve that but as I mentioned, all those roads lead to variations of Scott’s solution. And that isn’t the point of this post.

The point is to show how adjusting the structure of a Rule applying DRY can help one consolidate and overcome lots of repetitive code.

@rikoshak Thanks for the advise.
I figured the best way to fully understand your approach was to try it on another rule set and convert it to use your technicians.

The rule below is my failed attempt to convert an existing rule into the type you provided and it seems to get stuck around the case statements and errors regarding null.

LOG

2018-03-31 12:09:32.150 [INFO ] [arthome.model.script.BR4_Humid.rules] - Humidity Room, actioning: BR4

2018-03-31 12:09:32.160 [INFO ] [arthome.model.script.BR4_Humid.rules] - Checkpoint 0: 

2018-03-31 12:09:32.173 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Humid Action': An error occurred during the script execution: Could not invoke method: org.eclipse.smarthome.model.script.lib.NumberExtensions.operator_equals(java.lang.Number,java.lang.Number) on instance: null

RULE

rule "Humid Action"
when
Item BR1_Humid received update or
Item BR1_Humid_State changed or
Item BR2_Humid received update or
Item BR2_Humid_State changed or
Item BR3_Humid received update or
Item BR3_Humid_State changed or
Item BR4_Humid received update or
Item BR4_Humid_State changed or
Item LR_Humid received update or
Item LR_Humid_State changed or
Item GR_Humid received update or
Item GR_Humid_State changed or
Item HO_Humid received update or
Item HO_Humid_State changed
then
	val sourceRoom = triggeringItem.name.split("_").get(0)
	logInfo(sourceRoom + "_Humid.rules", "Humidity Room, actioning: " + sourceRoom)
    var _Humid = null
    var _State = null
    var _Heater = null
	logInfo(sourceRoom + "_Humid.rules", "Checkpoint 0: ")
    switch triggeringItem.name {
        case "BR1_Humid" : { _Humid = BR1_Humid_auto _State = BR1_Humid_State _Heater = MQTTBR1Heater}
        case "BR2_Humid" : { _Humid = BR2_Humid_auto _State = BR2_Humid_State _Heater = MQTTBR2Heater}
        case "BR3_Humid" : { _Humid = BR3_Humid_auto _State = BR3_Humid_State _Heater = MQTTBR3Heater}
        case "BR4_Humid" : { _Humid = BR4_Humid_auto _State = BR4_Humid_State _Heater = MQTTBR4Heater}
        case "LR_Humid" : { _Humid = LR_Humid_auto _State = LR_Humid_State _Heater = MQTTLRHeater}
        case "GR_Humid" : { _Humid = GR_Humid_auto _State = GR_Humid_State _Heater = MQTTGRHeater}
        case "HO_Humid" : { _Humid = HO_Humid_auto _State = HO_Humid_State _Heater = MQTTHOHeater}
    }
	
	if(_Humid == null || _State == null || _Heater == null) {
        logError(sourceRoom + "_Humid.rules", "Unexpectedly reached default case")
        return;
    }
	logInfo(sourceRoom + "_Humid.rules", "Checkpoint 1: ")
	
	logInfo(sourceRoom + "_Humid.rules", "Measured: " + triggeringItem.name.state)
	//logInfo(sourceRoom + "_Humid.rules", sourceRoom + " Humidity: Measured: " + sourceRoom + "_Humid.state" + " Max: " + sourceRoom + "_Max_Humid.state" + " Tgt: " + sourceRoom + "_Tgt_Humid.state" + " Min: " + sourceRoom + "_Min_Humid.state" + " State: " + sourceRoom + "_Humid_State.state")
	if (_Humid.state == ON) {
		logInfo(sourceRoom + "_Humid.rules", "Checkpoint 2: ")
		// set actions if we're asleep
		if (sleep_state.state == OFF ) {
			if (_State.state == "NULL" || _State.state == "DRY" || _State.state == "NORMAL" || _State.state == "DAMP")  {
				sendCommand(_Heater, OFF)} 
			else {sendCommand(_Heater, ON)}
		}
		
		// set actions if we're awake
		if (sleep_state.state == ON ) {	
			if (_State.state == "NULL" || _State.state == "DRY" || _State.state == "NORMAL") {
				sendCommand(_Heater, OFF)} 
			else {sendCommand(_Heater, ON)}
		}
	}
	else if (_Humid.state != ON) {
		logInfo(sourceRoom + "_Humid.rules", sourceRoom + " Humid control is disabled so no action taken") 
	}
end

[EDIT] Rule was updated with some corrections, the same error occurs.

Paul

When comparing to null, you need to use ===. I suspect this is the source of the problem. I’ve a long post about it somewhere but the tl;dr version of it is == is the same as call the .equals method on an object but null is a primitive and doesn’t have a .equals method to call so we have to use === which looks to see if the two operands have the same address in memory.

You should be seeing warnings in openhab.log when this rule is loaded that mentions you should use === with null.

All that being said, I want to emphasize that Scott’s approach is the better one and it should work here as well. Though because you need access to the Item themselves you will want to use the Group method in the Associated Item DP. Put all your Humid_auto Items into a Group and do the same for State (we only need the name of _Heater) and the rule collapses to:

then
    val sourceRoom = triggeringItem.name.split("_").get(0)
    logInfo(sourceRoom + "_Humid.rules", "Humidity Room, actioning: " + sourceRoom)
    logInfo(sourceRoom + "_Humid.rules", "Checkpoint 0: ")
    val _Humid = HumidAutos.members.findFirst[ auto | auto.name == sourceRoom+"_Humid_auto" ]
    val _State = HumidStates.members.findFrist[ state | state.name == sourceRoom+"_Humid_State" ]
    val _Heater = "MQTT"+sourceRoom+"Heater"

    if(_Humid === null || _State === null || _Heater === null) {
        logError(sourceRoom + "_Humid.rules", "Unexpectedly reached default case")
        return;
    }

    logInfo(sourceRoom + "_Humid.rules", "Checkpoint 1: ")

    logInfo(sourceRoom + "_Humid.rules", "Measured: " + triggeringItem.name.state)

    // the bottom part of the rule remains the same
    ...
end
1 Like