Thermal Strategy - A Four Season, Multi-Purpose, Dwelling Comfort System

Introduction

For those of us who live in the more Mediterranean and temperate climates, we can sometimes have quite complex thermal management behaviours that vary with the seasons. Here in Perth, Western Australia we have a long hot summer featuring cooling evening sea breezes and some heat waves; a shorter, cold and wet winter; and extended, temperate inter-seasons with sunny days, cool nights and cyclical [warm|cool]-wet/cold-dry periods. Our homes are predominantly concrete pads, double-brick walls, corrugated iron or clay/cement tiled roofs, high thermal mass, slow to change temperature, and quite easy to seal well. We use substantial amounts of energy on cooling in summer and even more on heating in winter. Most houses use ducted or split-system reverse-cycle air conditioning although ducted evaporative cooling is also popular. Central heating is practically unheard of.

In this climate I have developed some automation rules that I call “Thermal Strategy”, which is an attempt to codify the learned behaviours I have acquired living here and give the automation system a similar “driving style” to what I do when I’m manually in charge of things. The end-game is for the automation system to use its available sensors and actuators to maintain the house at a comfortable temperature with as little input from the occupants as possible, as unobtrusively as possible, but also nudging them into action when it’s required.

Motivations

This strategy is primarily motivated by saving energy, while also remaining comfortable. It is predicated on sparse use of air conditioning, the house occupants wearing some season-appropriate clothing, having a moderate tolerance for seasonal temperature changes, and being willing to interact with the house sometimes to effect outcomes. Rigidly maintaining internal temperature is not the goal here (though a far easier automation task).

Inputs

The strategy is built upon some simple inputs:

  • A target temperature for the living area (TS_SetPoint in this example, set as a constant in the rule)
  • The current temperature in the living area (Lounge_AirTemp)
  • The current temperature outside (OutsideAirTemp)
  • The forecast maximum temperature for the current day (BOM_Temp_Max_0)
  • The state of external doors and windows, or active ventilation (gDoorsWindows contacts)
  • The availability of excess energy production from a solar PV system (cER_EnergyAvailable switch)

Outputs

Currently the outputs are quite limited:

  • IR control of a reverse-cycle split system air conditioner (AC_Lounge_*)
  • Push notifications for occupants’ smartphones (Pushover action, gated by presence detection since it’s not sensible to tell someone to open a door if they’re not at home)
  • Dashboard displays in common areas using Grafana/Habpanel
  • Twitter updates
  • Log entries

[Obtaining these inputs and outputs is left as an exercise for the reader. Personally I’m using a lot of MySensors DIY devices via MQTT, http binding/JSONPATH transformation, OwnTracks presence, and some Python scripts I found here on the forums.]

Behaviours

  • When inside is colder than desired (typically during winter):
    ** If it’s warm outside, turn off heaters and open up doors and windows (typically during the day if it’s sunny)
    ** If it’s cold outside, close the doors and windows
    ** If spare energy is available from the solar system, do some free heating
    ** If very cold inside, use the A/C for heating
  • When inside is warmer than desired (typically during summer):
    ** If it’s cooler outside, turn off A/C and open up doors and windows (typically in the afternoon near the coast, or at night)
    ** If it’s hot outside, close the doors and windows
    ** If spare energy is available from the solar system, do some free cooling
    ** If very hot inside, use the A/C for cooling
  • When the outside temperature transitions from adverse to favourable or vice versa, notify the occupants to take appropriate action.
  • Doors and windows need to stay closed when heating/cooling. If any of them get opened for longer than 5 minutes, turn A/C off.
  • While the house remains sealed, occupants are free to manually control air conditioning units within a reasonable tolerance if short-term override is required. Wasteful use would be squashed by repeated automatic shut down.

Architecture

The core of the strategy is built around a finite state machine with the following states:

  • Cooling: Active use of air conditioning to cool, dwelling remains sealed.
  • Heating: Active use of air conditioning to heat, dwelling remains sealed.
  • Vented: Passive heating/cooling by opening/activating exterior ventilation.
  • Sealed: Ventilation closed to retain heat/cool but no active air conditioning (ERV active if present).
  • Close: Some doors and windows are open but they should be closed now.
  • Open: All doors and windows are closed but some should be opened now.

Processing the state machine is triggered by changes in internal temperature, door/window status or 30 minute timeout since the last process.

The transition between these states triggers various actuator rules called plans, that perform the work of the strategy. These outputs would need customising to meet the needs of each dwelling system.

There are some hard-coded values around how far the inside temp deviates from the set point before the strategy takes action. These have been tuned to match my preferences but would benefit from some tweaking if you find yourself feeling too hot or cold at any point.

Expansion

  • In future it’s anticipated that many windows in the house can be controlled automatically by motor actuators, eliminating the need for humans to be told to open/close them.
  • An active energy recovery ventilator/whole house fan is also in the works, which will largely be driven by this strategy.
  • Automated blinds could be incorporated, using an external lux sensor and time-of-day based sun movements to maximise solar gain in winter or heat rejection in summer.
  • Additional rooms serviced by additional split-system air conditioners will probably be added later.
  • Adjusting the strategy’s temperature set point could be used to maintain an empty house above freezing if it’s to be unoccupied for a long time.

Conclusion

As you can see this is quite a complicated bit of automation, but the result is quite elegant and minimally obtrusive I think. I’ve lived with it for a year now so most parts of its behaviour are well known to me and it’s fine-tuned my sense of how to operate the house efficiently. It’s a solid framework to build more automated features on top of and provide control for future actuators to be included.

I welcome any and all feedback.

Items

Items make extensive use of groups for averaging or logical operation. Putting the various sensors into these groups drives the core of the inputs like so:

Group:Number:AVG Lounge_AirTemp "Lounge Temperature [%.1f °C]" <temperature> (gLounge, gSiriThermostat, gInflux, gInsideTemp) ["CurrentTemperature"] 
Number EnvB_US_Temp "Upstairs EnviroButton Temperature [%.1f °C]" <temperature> (Lounge_AirTemp, gEnvB, gInfluxSlow) {mqtt="<[MQTT:sensors-out/5/0/1/0/0:state:default]",expire="6h"}
Number MS0_Temp "MultiSensor 0 Temperature" <temperature> (Lounge_AirTemp, gMS0, gInfluxSlow) {mqtt="<[MQTT:sensors-out/10/0/1/0/0:command:default]",expire="190m"}

Group:Number:AVG OutsideAirTemp "Outside Air Temp [%.1f °C]" <temperature> (gInfluxSlow)
Number BOM_Temp_Max_0 "Today Max [%d °C]" <temperature> (gInfluxSlow)
Number WX_Temp_DS "Outside Air Temp - DS [%.2f °C]" <temperature> (gWeatherStation, gInflux, OutsideAirTemp) {mqtt="<[MQTT:rs485-out/8/5/1/0/0:state:default]",expire="10m"}
Number WX_Temp_DHT "Outside Air Temp - DHT [%.2f °C]" <temperature> (gWeatherStation, gInflux, OutsideAirTemp) {mqtt="<[MQTT:rs485-out/8/6/1/0/0:state:default]",expire="10m"}
Number WX_Temp_BME "Outside Air Temp - BME [%.2f °C]" <temperature> (gWeatherStation, gInflux, OutsideAirTemp) {mqtt="<[MQTT:rs485-out/8/7/1/0/0:state:default]",expire="10m"}
Number bom_AirTemp "Air Temperature [%.1f °C]" <temperature> (OutsideAirTemp, gInfluxSlow){ http="<[bomObs:30000:JSONPATH($.observations.data[0].air_temp)]" }

Group:Contact:OR(OPEN,CLOSED) gDoorsWindows "Doors and Windows"(gAll, gInfluxSlow)
Contact LoungeDoor "Lounge Door Contact" <door> (gLounge, gDoorsWindows, gInfluxSlow)
Contact Entry_Door "Entry Main Door [%s]" <frontdoor> (gDining, gDoorsWindows) {mqtt="<[MQTT:sensors-out/23/2/1/0/16:state:MAP(01toOPENCLOSED.map)]"}

Core State Machine Rule

A number item represents the current state of the strategy

Number ThermalStrategy "Thermal Strategy [MAP(thermal.map):%s]" <climate> (gInfluxSlow)

thermal.map:

0=Err
1=Cooling
2=Heating
3=Vented
4=Sealed
5=Close
6=Open
-=-
undefined=-
uninitialized=-
NULL=-

Rule:

import java.util.concurrent.locks.ReentrantLock

val Number TS_SetPoint = 23.0 // Interior temperature basis point for thermal strategy

var Timer tim_Door_Notify = null
var Timer tim_ThermalStrategy = null
var Number prevStrat = 0
var Boolean stratChanged = false
val ReentrantLock lock = new ReentrantLock

// ---- Thermal Strategy ----
rule "Thermal Strategy State Machine" 
// 1 is "Cooling", 2 is "Heating", 3 is "Vented", 4 is "Sealed", 5 is "Close", 6 is "Open"
when
	Item Lounge_AirTemp changed or
	Item gDoorsWindows changed or
	Item Trig_Thermal changed to ON
then
	try {
		lock.lock()
		if(tim_ThermalStrategy !== null) {
			tim_ThermalStrategy.cancel()
			tim_ThermalStrategy = null
			//logInfo("Thermal","Thermal Timer Cancelled")
		}
		val LogTempOAT = String::format("%.1f", (OutsideAirTemp.state as DecimalType).floatValue)
		//logInfo("Thermal","Thermal Strategy Init: " + transform("MAP","thermal.map",ThermalStrategy.state.toString) + ", Lounge: " + Lounge_AirTemp.state.toString + " °C, Outside: " + LogTempOAT + " °C")
		var strat = 0 // Error
		var String minutes = now.getMinuteOfHour.toString
		if(now.getMinuteOfHour < 10) minutes = "0" + minutes
		val String timeNow = now.getHourOfDay.toString + ":" + minutes
		
// Cooling ----------
		if(ThermalStrategy.state == 1) {
			if(gDoorsWindows.state == OPEN) {
				prevStrat = 1
				strat = 5 // Close
			} else if(OutsideAirTemp.state < (Lounge_AirTemp.state as Number - 1)) {
				stratChanged = true
				strat = 6 // Open
			} else if(Lounge_AirTemp.state < (TS_SetPoint + 2)) {
				stratChanged = true
				strat = 4 // Sealed
			} else {
				strat = 1 // Cooling - no change
				Trig_Cooling.sendCommand(ON)
			}
		
// Heating ----------
		} else if(ThermalStrategy.state == 2) {
			if(gDoorsWindows.state == OPEN) {
				prevStrat = 2
				strat = 5 // Close
			} else if(OutsideAirTemp.state > (Lounge_AirTemp.state as Number + 1)) {
				stratChanged = true
				strat = 6 // Open
			} else if(Lounge_AirTemp.state > (TS_SetPoint - 3)) {
				stratChanged = true
				strat = 4 // Sealed
			}  else {
				strat = 2 // Heating - no change
				Trig_Heating.sendCommand(ON)
			}
		
// Vented ----------
		} else if(ThermalStrategy.state == 3) {
			if(gDoorsWindows.state == CLOSED) {
				val diff = OutsideAirTemp.state as Number - Lounge_AirTemp.state as Number
				if(diff > -1 && diff < 1) {
					// logInfo("Thermal","Vented -> Sealed by door closing accepted, temp difference " + diff + " °C")
					strat = 4 // Sealed
				} else strat = 6 // Open
			} else if(Lounge_AirTemp.state < TS_SetPoint && BOM_Temp_Max_0.state < (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state < (Lounge_AirTemp.state as Number - 1.1)) {
					prevStrat = 4 // new strat will be Sealed
					stratChanged = true
					strat = 5 // Close
				} else strat = 3 // Vented - no change
			} else if(Lounge_AirTemp.state >= TS_SetPoint && BOM_Temp_Max_0.state >= (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state > (Lounge_AirTemp.state as Number + 1.1)) {
					prevStrat = 4 // New strat will be Sealed
					stratChanged = true
					strat = 5 // Close
				} else strat = 3 // Vented - no change
			} else strat = 3 // Vented - no change
		
// Sealed ----------
		} else if(ThermalStrategy.state == 4) {
			if(gDoorsWindows.state == OPEN) {
				val diff = OutsideAirTemp.state as Number - Lounge_AirTemp.state as Number
				if(diff > -1.2 && diff < 1.2) {
					// logInfo("Thermal","Sealed -> Vented by door opening accepted, temp difference " + diff + " °C")
					strat = 3 // Vented
				} else {
					prevStrat = 4
					strat = 5 // Close
				}
			} else if(Lounge_AirTemp.state < TS_SetPoint && BOM_Temp_Max_0.state < (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state > (Lounge_AirTemp.state as Number + 1.1)) {
					strat = 6 // Open
				} else if(Lounge_AirTemp.state < (TS_SetPoint - 10)) {
					stratChanged = true
					strat = 2 // Heating
				} else if(Lounge_AirTemp.state < (TS_SetPoint - 7) && cER_EnergyAvailable.state == OFF) {
					stratChanged = true
					strat = 2 // Heating
				} else strat = 4 // Sealed - no change
			} else if(Lounge_AirTemp.state >= TS_SetPoint && BOM_Temp_Max_0.state >= (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state < (Lounge_AirTemp.state as Number - 1.1)) {
					strat = 6 // Open
				} else if(Lounge_AirTemp.state > (TS_SetPoint + 7)) {
					stratChanged = true
					strat = 1 // Cooling
				} else if(Lounge_AirTemp.state > (TS_SetPoint + 3) && cER_EnergyAvailable.state == OFF) {
					stratChanged = true
					strat = 1 // Cooling
				} else strat = 4 // Sealed - no change
			} else strat = 4 // Sealed - no change
		
// Close ----------
		} else if(ThermalStrategy.state == 5) {
			if(gDoorsWindows.state == CLOSED) {
				strat = prevStrat // Return to previous <or new> strategy
				if(tim_Door_Notify !== null) {
					tim_Door_Notify.cancel()
					tim_Door_Notify = null
					logInfo("Thermal","Notify Timer Cancelled")
				}
			} else if(Lounge_AirTemp.state < TS_SetPoint && BOM_Temp_Max_0.state < (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state > (Lounge_AirTemp.state as Number + 1.1)) {
					stratChanged = true
					strat = 3 // Vented
				} else strat = 5 // Close - no change
			} else if(Lounge_AirTemp.state >= TS_SetPoint && BOM_Temp_Max_0.state >= (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state < (Lounge_AirTemp.state as Number - 1.1)) {
					stratChanged = true
					strat = 3 // Vented
				} else strat = 5 // Close - no change
			} else strat = 5 // Close - no change
		
// Open ----------
		} else if(ThermalStrategy.state == 6) {
			if(gDoorsWindows.state == OPEN) {
				strat = 3 // Vented
				if(tim_Door_Notify !== null) {
					tim_Door_Notify.cancel()
					tim_Door_Notify = null
					logInfo("Thermal","Notify Timer Cancelled")
				}
			} else if(Lounge_AirTemp.state < TS_SetPoint && BOM_Temp_Max_0.state < (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state < (Lounge_AirTemp.state as Number - 1.1)) {
					stratChanged = true
					strat = 4 // Sealed
				} else strat = 6 // Open - no change
			} else if(Lounge_AirTemp.state >= TS_SetPoint && BOM_Temp_Max_0.state >= (TS_SetPoint + 3)) {
				if(OutsideAirTemp.state > (Lounge_AirTemp.state as Number + 1.1)) {
					stratChanged = true
					strat = 4 // Sealed
				} else strat = 6 // Open - no change
			} else strat = 6 // Open - no change
// Error ----------			
		} else {
			if(gDoorsWindows.state == CLOSED) strat = 4 // Sealed
			if(gDoorsWindows.state == OPEN) strat = 3 // Vented
			stratChanged = true
		}
		
		//logInfo("Thermal","New strat: " + transform("MAP","thermal.map",strat.toString))

		if(ThermalStrategy.state != strat) {
				ThermalStrategy.sendCommand(strat)
		} else {
			// logInfo("Thermal",timeNow + " - No change")
		}
		if(stratChanged) {
			stratChanged = false
			logInfo("Thermal",timeNow + " Lounge: " + Lounge_AirTemp.state.toString + " °C, Outside: " + LogTempOAT + " °C " + "Strategy: " + transform("MAP","thermal.map",strat.toString))
			sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + " °C, Outside: " + LogTempOAT + " °C " + "Strategy: " + transform("MAP","thermal.map",strat.toString))
		}
		Trig_Thermal.postUpdate(OFF)
		tim_ThermalStrategy=createTimer(now.plusMinutes(30), [ |
				tim_ThermalStrategy = null
				//logInfo("Thermal","Thermal Strategy Triggered by Timer")
				Trig_Thermal.sendCommand(ON)
			 ])
	} catch(Throwable t){}
	finally {
		lock.unlock()
	}
end

Plan Rules

These rules fire when the strategy changes, but also regularly during the execution of the strategy to maintain the system. Some trigger items:

Switch Trig_Thermal "Thermal Strategy Trigger"
Switch Trig_Cooling "Cooling Plan Trigger"
Switch Trig_Heating "Heating Plan Trigger"
Switch Trig_Vent "Vent Plan Trigger"
Switch Trig_Seal "Seal Plan Trigger"
rule "Cooling Plan"
when
	Item ThermalStrategy changed to 1 or 
	Item Trig_Cooling changed to ON
then
	var String minutes = now.getMinuteOfHour.toString
	if(now.getMinuteOfHour < 10) minutes = "0" + minutes
	val String timeNow = now.getHourOfDay.toString + ":" + minutes
	if(AC_Lounge_Switch.state == ON) {
		//logInfo("Thermal","A/C is On - Cooling")
	} else if(Lounge_AirTemp.state > (TS_SetPoint + 7)) {
		AC_Lounge_SetPoint.sendCommand(TS_SetPoint)
		AC_Lounge_Mode.sendCommand(3)
		AC_Lounge_FanSpeed.sendCommand(5)
		AC_Lounge_Patrol.sendCommand(OFF)
		AC_Lounge_Switch.sendCommand(ON)
		AC_Lounge_Economy.sendCommand(OFF)
		logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C - A/C On Cooling")
		sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C - A/C On Cooling")
	} else if(Lounge_AirTemp.state > (TS_SetPoint + 3)) {
		if(cER_EnergyAvailable.state == ON) {
			AC_Lounge_SetPoint.sendCommand(TS_SetPoint)
			AC_Lounge_Mode.sendCommand(3)
			AC_Lounge_FanSpeed.sendCommand(0)
			AC_Lounge_Patrol.sendCommand(OFF)
			AC_Lounge_Switch.sendCommand(ON)
			AC_Lounge_Economy.sendCommand(OFF)
			logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C & HWS Cut Out - A/C On Cooling")
			sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C & Energy Available - A/C On Cooling")
		}
	} else if(BOM_Temp_Max_0.state > (TS_SetPoint + 13)) {
		AC_Lounge_SetPoint.sendCommand(TS_SetPoint + 1)
		AC_Lounge_Mode.sendCommand(3)
		AC_Lounge_FanSpeed.sendCommand(0)
		AC_Lounge_Patrol.sendCommand(OFF)
		AC_Lounge_Switch.sendCommand(ON)
		AC_Lounge_Economy.sendCommand(ON)
		logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C - A/C On Eco Cooling")
		sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C - A/C On Eco Pre-Cooling")
	} else if(BOM_Temp_Max_0.state > (TS_SetPoint + 6)) {
		if(cER_EnergyAvailable.state == ON) {
			AC_Lounge_SetPoint.sendCommand(TS_SetPoint + 1)
			AC_Lounge_Mode.sendCommand(3)
			AC_Lounge_FanSpeed.sendCommand(0)
			AC_Lounge_Patrol.sendCommand(OFF)
			AC_Lounge_Switch.sendCommand(ON)
			AC_Lounge_Economy.sendCommand(ON)
			logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C & HWS Cut Out - A/C On Eco Cooling")
			sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C & Energy Available - A/C On Eco Pre-Cooling")
		}
	}
	Trig_Cooling.postUpdate(OFF)
end

rule "Heating Plan"
when
	Item ThermalStrategy changed to 2 or
	Item Trig_Heating changed to ON
then
	var String minutes = now.getMinuteOfHour.toString
	if(now.getMinuteOfHour < 10) minutes = "0" + minutes
	val String timeNow = now.getHourOfDay.toString + ":" + minutes
	if(AC_Lounge_Switch.state == ON) {
		//logInfo("Thermal","A/C is On - Heating")
	} else if(Lounge_AirTemp.state < (TS_SetPoint - 10)) {
		AC_Lounge_SetPoint.sendCommand(TS_SetPoint)
		AC_Lounge_Mode.sendCommand(2)
		AC_Lounge_FanSpeed.sendCommand(5)
		AC_Lounge_Patrol.sendCommand(OFF)
		AC_Lounge_Switch.sendCommand(ON)
		AC_Lounge_Economy.sendCommand(OFF)
		logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C - A/C On Heating")
		sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C - A/C On Heating")
	} else if(Lounge_AirTemp.state < (TS_SetPoint - 7)) {
		if(cER_EnergyAvailable.state == ON) {
			AC_Lounge_SetPoint.sendCommand(TS_SetPoint)
			AC_Lounge_Mode.sendCommand(2)
			AC_Lounge_FanSpeed.sendCommand(0)
			AC_Lounge_Patrol.sendCommand(OFF)
			AC_Lounge_Switch.sendCommand(ON)
			AC_Lounge_Economy.sendCommand(OFF)
			logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C & HWS Cut Out - A/C On Heating")
			sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C & Energy Available - A/C On Heating")
		}
	} else if(BOM_Temp_Max_0.state < (TS_SetPoint - 7)) {
		AC_Lounge_SetPoint.sendCommand(TS_SetPoint)
		AC_Lounge_Mode.sendCommand(2)
		AC_Lounge_FanSpeed.sendCommand(0)
		AC_Lounge_Patrol.sendCommand(OFF)
		AC_Lounge_Switch.sendCommand(ON)
		AC_Lounge_Economy.sendCommand(ON)
		logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C - A/C On Eco Heating")
		sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C - A/C On Eco Pre-Heating")
	} else if(BOM_Temp_Max_0.state < (TS_SetPoint - 5)) {
		if(cER_EnergyAvailable.state == ON) {
			AC_Lounge_SetPoint.sendCommand(TS_SetPoint - 1)
			AC_Lounge_Mode.sendCommand(2)
			AC_Lounge_FanSpeed.sendCommand(0)
			AC_Lounge_Patrol.sendCommand(OFF)
			AC_Lounge_Switch.sendCommand(ON)
			AC_Lounge_Economy.sendCommand(ON)
			logInfo("Thermal","Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C & HWS Cut Out - A/C On Eco Heating")
			sendTweet(timeNow + " Lounge: " + Lounge_AirTemp.state.toString + "°C & Forecast: " + BOM_Temp_Max_0.state.toString + "°C & Energy Available - A/C On Eco Pre-Heating")
		}
	}
	Trig_Heating.postUpdate(OFF)
end

rule "Vent Plan"
when
	Item ThermalStrategy changed to 3
then
	var String minutes = now.getMinuteOfHour.toString
	if(now.getMinuteOfHour < 10) minutes = "0" + minutes
	val String timeNow = now.getHourOfDay.toString + ":" + minutes
	if(AC_Lounge_Switch.state == ON) {
		AC_Lounge_Switch.sendCommand(OFF)
		logInfo("Thermal","Lounge A/C Off")
		sendTweet(timeNow + " Lounge A/C Off")
	}
	if(gDoorsWindows.state == CLOSED) {
		ThermalStrategy.sendCommand(6) // Open
	}
end

rule "Seal Plan"
when
	Item ThermalStrategy changed to 4
then
	var String minutes = now.getMinuteOfHour.toString
	if(now.getMinuteOfHour < 10) minutes = "0" + minutes
	val String timeNow = now.getHourOfDay.toString + ":" + minutes
	if(AC_Lounge_Switch.state == ON) {
		AC_Lounge_Switch.sendCommand(OFF)
		logInfo("Thermal","Lounge A/C Off")
		sendTweet(timeNow + " Lounge A/C Off")
	}
	if(gDoorsWindows.state == OPEN) {
		ThermalStrategy.sendCommand(5) // Close
	}
end

rule "Close Plan"
when
	Item ThermalStrategy changed to 5
then
	logInfo("Thermal","Started timer: 5 minutes til 'Close' notification")
	tim_Door_Notify=createTimer(now.plusMinutes(5), [ |
			var String minutes = now.getMinuteOfHour.toString
			if(now.getMinuteOfHour < 10) minutes = "0" + minutes
			val String timeNow = now.getHourOfDay.toString + ":" + minutes
			logInfo("Thermal","Close the doors and windows")
			if(AC_Lounge_Switch.state == ON) {
				AC_Lounge_Switch.sendCommand(OFF)
				logInfo("Thermal","Lounge A/C Off")
				sendTweet(timeNow + " Thermal Strategy: Close the doors and windows - Lounge A/C switched off")
			} else sendTweet(timeNow + " Thermal Strategy: Close the doors and windows")
			// If Cary is home, notify to close doors and windows
			if(gCaryPresence.state == ON) {
				sendPushoverMessage(pushoverBuilder("Close the doors and windows"))
			}
			tim_Door_Notify = null
		 ])
end

rule "Open Plan"
when
	Item ThermalStrategy changed to 6
then
	logInfo("Thermal","Started timer: 5 minutes til 'Open' notification")
	tim_Door_Notify=createTimer(now.plusMinutes(5), [ |
			var String minutes = now.getMinuteOfHour.toString
			if(now.getMinuteOfHour < 10) minutes = "0" + minutes
			val String timeNow = now.getHourOfDay.toString + ":" + minutes
			logInfo("Thermal","Open the doors and windows")
			if(AC_Lounge_Switch.state == ON) {
				AC_Lounge_Switch.sendCommand(OFF)
				logInfo("Thermal","Lounge A/C Off")
				sendTweet(timeNow + " Thermal Strategy: Open the doors and windows - Lounge A/C switched off")
			} else sendTweet(timeNow + " Thermal Strategy: Open the doors and windows")
			// If Cary is home, notify to open doors and windows
			if(gCaryPresence.state == ON) {
				sendPushoverMessage(pushoverBuilder("Open the doors and windows"))
			}
			tim_Door_Notify = null
		 ])
end

Sitemap

Text item=Lounge_AirTemp label="Temperature [%.1f °C]"
Text item=OutsideAirTemp label="OAT [%.1f °C]"
Text item=ThermalStrategy
10 Likes