After finding nothing in the tutorials similar to my implementation, I decided to write a post about it.
I wanted to control the A/C based on time of day, but also allow a person to change the temperature on the thermostat, or in OpenHab.
Virtual setpoints for each air conditioner are set by time of day rules, manual changes at the thermostat or in OH change the virtual setpoint, and starts a timer. Changes to the virtual setpoint modify the Item setpoint.
items:
Number:Temperature vTNH "Temperature [%.1f °F]" (gVirtTemp) // virtual temperature north heating setpoint
Number:Temperature vTNC "Temperature [%.1f °F]" (gVirtTemp) // north cooling
Number:Temperature vTSH "Temperature [%.1f °F]" (gVirtTemp) // south heating
Number:Temperature vTSC "Temperature [%.1f °F]" (gVirtTemp) // south cooling
rules:
import java.util.Map
val Map<String, Timer> thermTimers = newHashMap
val Number northTimer = 120 // timeout to allow override at tstat
val Number southTimer = 240
val Number offMode = 0
// This lambda changes the virtual temps only when the thermostat mode is appropriate, and would cause a change in value.
val applyVirtualTemp = [Number tnh, Number tnc, Number tsh, Number tsc |
val Number coolMode = 2
val Number heatMode = 4 // 4-emergency heating strips
logInfo("applyVirtualTemp", "");
var currentMode = Thermostat_ThermostatMode.state as Number
var h = vTNH.state as Number
var c = vTNC.state as Number
if ((currentMode == heatMode) && (h != tnh)){vTNH.sendCommand(tnh)}
if ((currentMode == coolMode) && (c != tnc)){vTNC.sendCommand(tnc)}
currentMode = ZWaveNode019SouthThermostat_ThermostatMode.state as Number
h = vTSH.state as Number
c = vTSC.state as Number
if ((currentMode == heatMode) && (h != tsh)){vTSH.sendCommand(tsh)}
if ((currentMode == coolMode) && (c != tsc)){vTSC.sendCommand(tsc)}
]
// The cron based rules for changing the temperature NH NC SH SC
rule "time01" when Time cron "0 0 0 ? * MON-FRI *" then applyVirtualTemp.apply(50,80,60,76); end
rule "time02" when Time cron "0 0 7 ? * MON-FRI *" then applyVirtualTemp.apply(70,76,70,76); end
rule "time03" when Time cron "0 0 10 ? * MON-FRI *" then applyVirtualTemp.apply(64,79,60,80); end
rule "time04" when Time cron "0 0 12 ? * MON-FRI *" then applyVirtualTemp.apply(65,78,60,80); end
rule "time05" when Time cron "0 0 16 ? * MON-FRI *" then applyVirtualTemp.apply(69,76,60,78); end
rule "time06" when Time cron "0 0 21 ? * MON-FRI *" then applyVirtualTemp.apply(50,78,60,76); end
rule "time07" when Time cron "0 0 0 ? * SAT,SUN *" then applyVirtualTemp.apply(50,80,60,76); end
rule "time08" when Time cron "0 0 6 ? * SAT,SUN *" then applyVirtualTemp.apply(69,76,70,76); end
rule "time09" when Time cron "0 0 10 ? * SAT,SUN *" then applyVirtualTemp.apply(65,76,50,80); end
rule "time10" when Time cron "0 0 12 ? * SAT,SUN *" then applyVirtualTemp.apply(65,76,50,80); end
rule "time11" when Time cron "0 0 21 ? * SAT,SUN *" then applyVirtualTemp.apply(50,76,50,80); end
rule "Virtual temp updated"
when
// a group is more appropriate here
Item vTNC changed or
Item vTNH changed or
Item vTSC changed or
Item vTSH changed
then
var setptItem = Thermostat_SetpointCooling
if (triggeringItem.equals(vTNH)) {setptItem = Thermostat_SetpointHeating}
if (triggeringItem.equals(vTSC)) {setptItem = ZWaveNode019SouthThermostat_SetpointCooling}
if (triggeringItem.equals(vTSH)) {setptItem = ZWaveNode019SouthThermostat_SetpointHeating}
// if a timer was not set for a setpoint, then set the setpoint to the virtual value
if (thermTimers.get(setptItem.name) === null) {
logInfo("Logger", setptItem.name + " thermostat timer was not set, setting to default.")
setptItem.sendCommand(triggeringItem.state as Number);
} else {
logInfo("Logger", setptItem.name + " thermostat timer override, ignoring");
}
end
rule "Thermostat was changed"
when
Item Thermostat_SetpointCooling changed or
Item Thermostat_SetpointHeating changed or
Item ZWaveNode019SouthThermostat_SetpointCooling changed or
Item ZWaveNode019SouthThermostat_SetpointHeating changed
then
var Number timeout = northTimer
if (triggeringItem.name.contains("South")) {timeout = southTimer }
var GenericItem vItem
if (triggeringItem.equals(ZWaveNode019SouthThermostat_SetpointHeating)) {vItem = vTSH}
if (triggeringItem.equals(ZWaveNode019SouthThermostat_SetpointCooling)) {vItem = vTSC}
if (triggeringItem.equals(Thermostat_SetpointHeating)) {vItem = vTNH}
if (triggeringItem.equals(Thermostat_SetpointCooling)) {vItem = vTNC}
if (triggeringItem.state as Number != vItem.state as Number) {
logInfo("ThermostatWasChanged", "Setpoint and virtual temps are different, starting timer")
thermTimers.get(triggeringItem.name)?.cancel
thermTimers.put(triggeringItem.name, createTimer(now.plusMinutes(timeout), [ |
logInfo("ThermostatWasChanged", "Timer is finished.")
var GenericItem vItem1
if (triggeringItem.equals(ZWaveNode019SouthThermostat_SetpointHeating)) {vItem1 = vTSH}
if (triggeringItem.equals(ZWaveNode019SouthThermostat_SetpointCooling)) {vItem1 = vTSC}
if (triggeringItem.equals(Thermostat_SetpointHeating)) {vItem1 = vTNH}
if (triggeringItem.equals(Thermostat_SetpointCooling)) {vItem1 = vTNC}
triggeringItem.sendCommand(vItem1.state as Number)
thermTimers.remove(triggeringItem.name)
logInfo("ThermostatWasChanged", triggeringItem.name + " timed out, and setpoint temperature set to default.")
]))
} else {
logInfo("ThermostatWasChanged", "Setpoint and virtual temps are equal")
}
end
Improvements to this would be to create appropriate groups, and have a hashmap lookup of item to item, which would reduce code duplication.