Hi all,
I’m sharing with you here my heating setup in the hopes that it can help someone (and to just show it off, too, I guess.
My house is quite typical of UK houses, where I have a boiler, and one or more radiators in each room. The boiler is on/off only, and has a single thermostat, usually located in the worst possible room in the house. You then end up with a single thermostat controlling the temperature of the whole house. I had basic thermostatic valves on each radiator, but the overall on/off is still governed by the temperature of one room - so I often ended up with some rooms much colder than others.
About 18 months ago, I fitted a Nest, and whilst this gave some nice features, it still was a single thermostat, determining the status of the heating across the whole house, and it still had all the pitfalls of a basic setup - but instead I’ve spent £150 on a control unit rather than £20…
So, I started to look into some other options, including commercially available products - but they all had one or more things that I may not be able to live with. There’s plenty of radiator valves that will “zone” each room - but these still have the temperature sensor built into the valve - which is located right next to the heat source for the room! Some systems allow you to also add extra thermostats, but then it more than doubles the cost for each room.
So, I started to look into how I could do it cheaper.
This setup is based around a few core components:
Thermal Actuator
When power is applied, a small heating element is powered, which expands some special wax. This presses down a pin, opening the valve. This is completely silent in operation, and takes around 3 minutes to fully open or fully close.
https://www.amazon.co.uk/dp/B01HBCW5ES/
(UK link above dead, same product on Amazon Germany below)
Controllable relay
I am using the Sonoff Basic - there’s plenty of information here around them, and it’s quite easy to put custom firmware on them to remove the requirement for an internet connection.
Temperature Sensor
I am using a DHT22 via an ESP8266.
You could combine the relay & temperature sensor by using a Sonoff TH10/TH16 with temperature sensor plugged into it https://www.amazon.co.uk/dp/B06XSHMG8P/
My Hardware
It’s not too pretty, as I’m moving soon, but this is how it all is currently looking in one of my rooms.
And the NodeMCU/DHT22 are just loose - I am thinking of adding an OLED display and rotary encoder to provide some input & feedback, and encasing it in a box, but for now, there’s a few of these dotted around the house hidden out of sight behind photo frames etc!
All communication to the ESP/Sonoff is via MQTT.
openHAB Implementation
The openHAB side of the system is based upon Heating Boilerplate - A Universal Temperature Control Solution with Modes
Currently, each room has a single radiator, and a single temperature sensor. However, for future-proofing, I wanted to have one or more temperature sensors controlling one or more radiators.
The average temperature for a group of sensors will then be utilised to control all the radiators in a room/zone.
The following live in my heating_mode.items
file.
I start off with a group to contain everything related to heating, and list of prefixes which are applied to all items
// Parent group to contain all items
Group gHeating
// Prefixes
// OF = Office
// MB = Master Bedroom
// CB = Child Bedroom
// LR = Living Room
// DR = Dining Room
// KI = Kitchen
// BA = Bathroom
// DL = Downstairs Toilet
// HA = Hallway
// LA = Landing
// GA = Garage
// CE = Cellar
For brevity, I’ll only list a couple of rooms from now on to show the concepts.
I then have items which hold the current Target Temperature for each room/zone:
// Current target temperatures
Group:Number gHeatingTargetTemp (gHeating)
Number LR_Heating_TargetTemp "Living Room Heating Target [%.1f °C]" (gHeatingTargetTemp)
Number OF_Heating_TargetTemp "Office Heating Target [%.1f °C]" (gHeatingTargetTemp)
Number MB_Heating_TargetTemp "Master Bedroom Heating Target [%.1f °C]" (gHeatingTargetTemp)
The following is mostly from the Heating Boilerplate, so it’s better explained there, but this is the current heating mode, and items to contain the current preset temperatures.
String Heating_Mode "Global Heating Mode [MAP(heating_mode.map):%s]" <heating>
Switch Heating_UpdateHeaters "Send Target Temperatures to Heaters"
// Presets
Group Heating_PresetNormal_Group (gHeating)
Number LR_Heating_PresetTempNormal "Living Room Heating Preset (Normal Mode) [%.1f °C]" <heating> (Heating_PresetNormal_Group)
Number OF_Heating_PresetTempNormal "Office Heating Preset (Normal Mode) [%.1f °C]" <heating> (Heating_PresetNormal_Group)
Number MB_Heating_PresetTempNormal "Master Bedroom Heating Preset (Normal Mode) [%.1f °C]" <heating> (Heating_PresetNormal_Group)
Next comes my temperature sensors. Notice how I have listed 2 sensors in the Living Room.
// Temp sensors
Number OF_Temp1 "Office Temperature [%.1f °C]" <temperature> (gTemperature) {mqtt="<[mosquitto:esp-office-1/env/temperature:state:default]"}
Number LR_Temp1 "Living Room Temperature 1 [%.1f °C]" <temperature> (gTemperature) {mqtt="<[mosquitto:esp-livingroom-1/env/temperature:state:default]"}
Number LR_Temp2 "Living Room Temperature 2 [%.1f °C]" <temperature> (gTemperature) {mqtt="<[mosquitto:esp-livingroom-2/env/temperature:state:default]"}
Number MB_Temp1 "Master Bedroom Bedroom Temperature [%.1f °C]" <temperature> (gTemperature) {mqtt="<[mosquitto:esp-masterbedroom-1/env/temperature:state:default]"}
Finally in the heating_mode.items
file, are the Sonoff devices used to switch the Thermal Actuators on and off.
// Switch items used for actuators
Group:Switch:OR(ON, OFF) gHeatingActuators "The heating should be [%s]" <fire> (gHeating)
Switch OF_Heating_Actuator "Office Heating [%s]" <radiator> (gHeatingActuators) { mqtt=">[mosquitto:cmnd/sonoff-office-1/POWER:command:*:default], <[mosquitto:stat/sonoff-office-1/POWER:state:default]", autoupdate="false"}
Switch LR_Heating_Actuator1 "Living Room Heating 1 [%s]" <radiator> (gHeatingActuators) { mqtt=">[mosquitto:cmnd/sonoff-livingroom-1/POWER:command:*:default], <[mosquitto:stat/sonoff-livingroom-1/POWER:state:default]", autoupdate="false"}
Switch LR_Heating_Actuator2 "Living Room Heating 2 [%s]" <radiator> (gHeatingActuators) { mqtt=">[mosquitto:cmnd/sonoff-livingroom-2/POWER:command:*:default], <[mosquitto:stat/sonoff-livingroom-2/POWER:state:default]", autoupdate="false"}
Switch MB_Heating_Actuator "Master Bedroom Heating [%s]" <radiator> (gHeatingActuators) { mqtt=">[mosquitto:cmnd/sonoff-masterbedroom-1/POWER:command:*:default], <[mosquitto:stat/sonoff-masterbedroom-1/POWER:state:default]", autoupdate="false"}
As I’m using a Nest, these items are defined via PaperUI.
Rules
With all the items set up, it’s onto the Rules. Again, a lot of this comes from the Heating Boilerplate, so if you’ve not read that yet, go read it
My heating_mode.rules
file:
val String filename = "heating_mode.rules"
val Number hysteresis = 0.2
Sets up a variable for filename in logging, and one for Hysteresis - how far above & below the set-point do you want to swing. I originally had this at 0.5, but my wife could detect that 1 degree overall swing, and would keep coming to me saying “it’s gone cold” as it dropped towards the lower end! So I ended up on 0.2 - but it’s simple to change here.
Next up is how I look at the current temperatures, and target temperatures for each room/zone.
// ========================
// check Current temps and enables/disables actuators
rule "Check heating actuators every 5 minutes"
when
Time cron "0 0/5 * * * ?" or
Item gHeatingTargetTemp received update
then
var Number hylow = 0
var Number hyhigh = 0
gHeatingTargetTemp.members.forEach[ temp |
hylow = (temp.state as Number) - hysteresis
hyhigh = (temp.state as Number) + hysteresis
val String GroupName = temp.name.substring(0, 3)
val nums = gTemperature.allMembers.filter[i | i.name.contains(GroupName + "Temp")]
if (nums.size > 0) {
val avg = nums.map[state as Number].reduce[result, value | result = (result + value) ] / nums.size
gHeatingActuators.members.filter[i | i.name.contains(GroupName + "Heating_Actuator")].forEach [ i |
if (avg <= hylow) {
if (i.state == OFF) {
i.sendCommand(ON)
}
}
if (avg >= hyhigh) {
if (i.state == ON) {
i.sendCommand(OFF)
}
}
]
}
]
end
I am a verbose programmer - a Rules guru like Rich can probably condense this down to a fraction of the size
The concept is that every 5 minutes, for each prefix of current temperatures (ie LR_
) it will take the average temperature of all sensors, compare to the target, and will switch on or off each actuator item which also has the same prefix.
This is what will allow grouping of multiple sensors to multiple outputs.
As I’m using the Nest as a glorified on/off switch, I needed to add a rule to turn it on or off. This is essentially changing the target temperature to be either lower than (or equal to) my lowest target temperature, or higher than my highest temperature. I’ve also relocated it onto a windowsill in my kitchen, which is one of the the coldest places in the house.
This fires either when one of the actuators change state, when Away mode changes, and every 30 minutes (this may not be needed with the newer Nest binding, but I was trying it very early on during its testing and needed to prod it every few minutes)
rule "Adjust Nest based on Actuators"
when
Time cron "30 0 0/1 * * ?" or
Item gHeatingActuators received update or
Item Nest_away received update
then
if (gHeatingActuators.state == ON) {
if (Nest_away.state == "HOME") {
// Nest thinks we are home, make sure the heating is on!
if (Nest_hvac_mode.state == "eco") {
Nest_hvac_mode.sendCommand(Nest_previous_mode.state)
Thread::sleep(500)
}
if (Nest_hvac_mode.state !== "eco") {
Nest_target_temperature_c.sendCommand(24.0)
}
} else {
// Nest thinks we are Away, probably shouldn't change that...
}
} else {
if (Nest_away.state == "HOME") {
// Nest thinks we are home, make sure the heating is on!
if (Nest_hvac_mode.state == "eco") {
Nest_hvac_mode.sendCommand(Nest_previous_mode.state)
Thread::sleep(500)
}
if (Nest_hvac_mode.state !== "eco") {
Nest_target_temperature_c.sendCommand(15.0)
}
} else {
// Nest thinks we are Away, probably shouldn't change that...
}
}
end
The rest of the Rules is mostly from the boilerplate, but with a few additions of extra cron jobs to change temperatures at a few more points during the day.
rule "Initialize uninitialized virtual Items"
when
System started
then
createTimer(now.plusSeconds(180)) [ |
logInfo(filename, "Executing 'System started' rule for Heating")
if (Heating_Mode.state == NULL) Heating_Mode.postUpdate("NORMAL")
Heating_PresetNormal_Group.members.filter[item | item.state == NULL].forEach[item | item.postUpdate(19.0)]
]
end
rule "React on heating mode switch, send target temperatures"
when
Item Heating_Mode received update or
Item Heating_UpdateHeaters received command ON
then
Heating_UpdateHeaters.postUpdate(OFF)
logInfo(filename, "Heating Mode: " + Heating_Mode.state)
switch Heating_Mode.state {
case "NORMAL": {
LR_Heating_TargetTemp.sendCommand(LR_Heating_PresetTempNormal.state as Number)
OF_Heating_TargetTemp.sendCommand(OF_Heating_PresetTempNormal.state as Number)
MB_Heating_TargetTemp.sendCommand(MB_Heating_PresetTempNormal.state as Number)
}
case "PARTY": {
LR_Heating_TargetTemp.sendCommand(21.0)
OF_Heating_TargetTemp.sendCommand(17.0)
MB_Heating_TargetTemp.sendCommand(19.0)
}
case "SICKDAY": {
LR_Heating_TargetTemp.sendCommand(23.0)
OF_Heating_TargetTemp.sendCommand(19.0)
MB_Heating_TargetTemp.sendCommand(23.0)
}
case "WEEKEND_TRIP": {
LR_Heating_TargetTemp.sendCommand(17.0)
OF_Heating_TargetTemp.sendCommand(17.0)
MB_Heating_TargetTemp.sendCommand(17.0)
}
case "AWAY": {
LR_Heating_TargetTemp.sendCommand(15.0)
OF_Heating_TargetTemp.sendCommand(15.0)
MB_Heating_TargetTemp.sendCommand(15.0)
}
case "OFF_SUMMER": {
LR_Heating_TargetTemp.sendCommand(6.0)
OF_Heating_TargetTemp.sendCommand(6.0)
MB_Heating_TargetTemp.sendCommand(6.0)
}
default : { logError(filename, "Heating Mode unknown: " + Heating_Mode.state) }
}
end
// ========================
// mode resets
rule "End PARTY and SICKDAY mode at 2:00 in the night"
when
Time cron "0 0 2 ? * * *"
then
if (Heating_Mode.state == "PARTY" || Heating_Mode.state == "SICKDAY") {
Heating_Mode.postUpdate("NORMAL")
}
end
rule "End WEEKEND_TRIP mode at 13:00 on Monday"
when
Time cron "0 0 13 ? * MON *"
then
if (Heating_Mode.state == "WEEKEND_TRIP") {
Heating_Mode.postUpdate("NORMAL")
}
end
// ========================
// NORMAL schedule
rule "6:30, weekdays"
when
Time cron "0 30 6 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(21.0)
OF_Heating_PresetTempNormal.postUpdate(21.0)
MB_Heating_PresetTempNormal.postUpdate(20.0)
Thread::sleep(500)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "8:00 weekdays"
when
Time cron "0 0 8 ? * * *"
then
MB_Heating_PresetTempNormal.postUpdate(18.0)
Thread::sleep(500)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "18:00"
when
Time cron "0 0 18 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(22.0)
OF_Heating_PresetTempNormal.postUpdate(17.0)
MB_Heating_PresetTempNormal.postUpdate(18.0)
Thread::sleep(500)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "20:30"
when
Time cron "0 30 20 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(22.0)
OF_Heating_PresetTempNormal.postUpdate(17.0)
MB_Heating_PresetTempNormal.postUpdate(20.0)
Thread::sleep(500)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "23:30"
when
Time cron "0 30 23 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(17.0)
OF_Heating_PresetTempNormal.postUpdate(17.0)
MB_Heating_PresetTempNormal.postUpdate(17.0)
Thread::sleep(500)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "9:00, weekend"
when
Time cron "0 0 9 ? * SAT-SUN *"
then
MB_Heating_PresetTempNormal.postUpdate(17.0)
Thread::sleep(500)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "6:31, Friday, Weekends, office"
when
Time cron "0 31 6 ? * FRI-SUN *"
then
OF_Heating_PresetTempNormal.postUpdate(17.0)
Thread::sleep(500)
Heating_UpdateHeaters.sendCommand(ON)
end
Results
The results from this is that I have the rooms at the exact temperature I want, when I want it - and since setting it up, I’ve barely touched anything. I do have the ability to change the target temperature on a per-room basis, but I’ve not really needed to.
You can really see how this works when you look at the below graphs from Grafana - it shows the target temperatures as dashed lines, the current temperature as a solid line, and the second graph shows when each actuator was on or off.
Within Basic UI I also have an overview:
And on a per-room basis, the ability to change the target and see the current temperature:
I hope this helps someone - it’s certainly working well for me over the last 3 months - I’ve just not got round to writing this up previously.
2019-09-24 Update
I’ve done a bit of tweaking and added some new functionality, updated Rules and Items are in post #75 (My Central Heating Solution using Thermal Actuators)