My Central Heating Solution using Thermal Actuators

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!

image

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 :slight_smile:

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 :blush:

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:

image

And on a per-room basis, the ability to change the target and see the current temperature:

image

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)

21 Likes

This is very good indeed sir,
I am in the UK too and I have set-up more or less the same system.
The sensors send info to OH every 5 mins. The temperature is compared against a target and the boiler turned on or off. Like you I am doing this with a Nest but I have an electrodragon relay with small OLED display in the works to control the boiler in the near future.

I use Node-Red for control the scheduling. In particular the node ramp-thermostat.
It gives my the ability to do something like this:

I store the different profiles according to rooms and house modes in a JSON file looking like this:

{ "profiles": {
    "MasterBedroom_NORMAL_WEEKDAY":
    {   "name":"MasterBedroom_NORMAL_WEEKDAY",
        "points": {
            "00:00":15.0,
            "05:30":15.0,
            "06:30":19.0,
            "10:00":19.0,
            "11:00":15.0,
            "16:00":15.0,
            "17:00":19.0,
            "19:00":19.0,
            "20:00":15.0,
            "24:00":15.0
        }
    },
    "MasterBedroom_NORMAL_WEEKEND":
    {   "name":"MasterBedroom_NORMAL_WEEKEND",
        "points": {
            "00:00":15.0,
            "09:30":15.0,
            "10:30":19.0,
            "13:30":19.0,
            "14:30":15.0,
            "24:00":15.0
        }

And now I have a different “program” for each room depending on house mode and time of the week…

I have not touched anything for months now except to reboot the sonoffs when I changed the MQTT broker.

Saving a lot on gas bills too…

Good write up although I was confused why you needed the thermal actuator. The DHT22 should have worked on its own.

How I would have done this:

ESP8266 - ~$4
DHT22 - ~$2
relay(SRD-05VDC-SL-A) - 20 cents
servo (MG996R maybe?) - $3
some capacitors and diodes - maybe 50 cents

total: ~$10

Im not sure how the thermal actuator work. The Amazon link doesn’t say much. Can you control how much the valve opens?

To know the position of valve using the servo I mentioned, just add a rotary encoder ( ~50 cents ) or use some mechanical switches for the endstop limits.

I don’t quite understand how you think that list of components will give the same solution - but I am happy to be given more choices.

The thermal actuator opens the valve on the radiator - this requires pressing down with some force.

I also wanted to de-couple the physical location of heat source and temperature sensing, hence the two separate bits of kit (Thermal Actuator/Sonoff, and ESP/DHT/OLED/Rotary Encoder/whatever else I wish to add to this part of it).

The prices of these parts can also be bought down if purchased from other sources - I just linked to Amazon for examples.

When I move, I may even run all the actuators back to a central point and use something other than Sonoff - this is easy to install and remove! :wink:

1 Like

Well, the esp will have all the logic. DHT22 will be the temp sensor, when it reaches your predefined threshold, it will activate the servo motor to turn (open or close) the valve. The servo I mentioned above has high torque should it be strong enough to turn even a water valve. My above post was incorrect, you actually dont need a relay. A mosfet should be enough to drive a servo. The rotary encoder will tell you how much you have opened (or closed) the valve.

I considered other options, but in the end you’ve got to factor in making all that fit onto a standard radiator valve… Which isn’t a rotary valve, it’s a tiny little spring-loaded pin. Add to that the Thermal Actuator is designed for this job, and looks fairly smart (and not home-made), and takes seconds to install. Once I hide the Sonoff and wiring, you’ll hardly know.

That also won’t help with decoupling the temperature sensors from the heat source, another key item for me.

If i had bought the items in bulk from another supplier, I could have cut the costs even more - but I’m still less than £30 per room, and there’s no commercially available solution I’ve found that is that cheap, and it’ll take me 5 minutes to put together the hardware for extra rooms as I expand the implementation :slight_smile:

Garry - very well put together solution and a great tutorial/write-up!

For your temperature sensors, you’re thinking exactly along the lines of what I came up with - a NodeMCU with a DHT22 and an OLED screen for local display. I don’t have a rotary encoder on mine, but you could easily add one to the unit. Feel free to use it as a starting point (and share any improvements you make to the Github repository) if you want to.

Here’s a short demo video I recently posted https://youtu.be/VefbT6RuT-4

P.S. I’m also loving the Grafana charts, I need to implement that for my 15 or so sensors :slight_smile:

Cheers!

just an additional option…
i was going to use OLED displays in each room with buttons for up/down temperature but as i added more and more devices to control it made more sense to take old phones and turn them into habpanels. Plus then i could modify stuff in other rooms for whatever room I was in. I’m not down-talking the OLED idea, it just may become inadequate as you do more and more.
this is an old S4 in a 3d printed case.

1 Like

good to know. For me, I don’t even mind CNC milling PCBs when prototyping. Completed products, I send to ITEAD. They do the work for me :wink:

Hmm… why doesn’t a DIY have to look home made? None of my work looks home made, but 90% of all my IoTs are indeed home-made. In fact, I still have some stuff still running for years, and they are still on a breadboard! Look at @bartus ’ link. It’s DIY but it looks commercial.

I only suggested ways on how to do it less expensively, especially since this sentence is on a line on its own :wink:

I fully agree with ur post above. But I won’t call it cheap :slight_smile: let’s settle with less expensive but reliable :slight_smile:

Liking this - how did you wire the power to the s4?

I soldered wires to the QI charger contacts. As long as you’re not moving there’s no worry of them coming loose. You could also get a qi pad and coil and go that route but I haven’t moved the phone since I put it on the wall so I’m not concerned

I am interested in using some of these actuators as a way of controlling radiators. The problem I have is that actuators seem to come in 2 different types - Normally open or Normally closed. Now to have the actuator ‘work’ and stay in its opposite position it needs to use power. I have seen some paperwork on different actuators stating 2 watts. Now that isn’t a lot but say I want to run a sonoff to control these too. Now I’m increasing power usage (possibly up to 4-5w). Now if we factor in each room (lets say 5 rooms), that is 25w of usage just for control and temperature reading which might not be really high but when you have other systems running can add to the total continuous power draw. I could be wrong on their power usage but it seems that is what I am reading…

My actuators are Normally Closed, so only if the heating is on will they draw power - they are rated 2W.

A quick Google suggests a Sonoff uses around a watt.

My wife says “I’d rather be warm than rich”, so the minor power draw and potentially increased gas usage from having the heating on to maintain the temperature we want when we want it was my overall goal here, not to try to save money :slight_smile: I know everyone has different priorities - for me, saving a bit of power/gas/money wasn’t a consideration.

I agreed overall with @Confused
The overall heating fuel savings made with the system will more than counterbalance the small extra electrical power needed
And your can always turn them off completely by hand when the heating is not needed and turn them back on in winter

1 Like

I know its an old post but try looking at underfloor heating wiring centers and manifold radiator heating systems.
the wiring center operates the actuators when a signal is present for its respective thermostat. In your case it could be signals from your NODE MCU. Thats a way of centralising kit.

Great idea.

Just be aware that the actuators being talked about here are super easy to fit and just work :smile:

If it helps, this wiring diagram might explain the logic a bit more.

http://www.mdar.co.uk/dl/Basic_Wiring_plan_V1.5.pdf

All the valves shown are Normally Closed, so only draw current when needed.

There is a minimal amount of energy needed to power the logic, but compared to yearly savings on energy, it is easily ignored.

Thanks a lot for the great write up @Confused!

I was looking for exactly the same solution: being able to individually control radiator thermostats at a normal price range. So I also bought the Eazy Systems Thermal Actuator, but I have issues with the installation. Was your install just plug and play (remove old thermostat, screw them on) as per the product description?

When I try to screw them on, I cannot seem to get a good fit. My old thermostat also seems to be M30 so I’m not sure what I am doing wrong. The visual install instructions are also not 100% clear to me.

Please see some pictures here: https://imgur.com/a/xkYCpNJ

When installing into the valve, I found it much easier to press the actuator hard onto the valve, then start tightening the screw. Without applying that pressure, it was difficult to get the thread started as you’re fighting against the pin in the valve :slight_smile:

1 Like

Switch them on and they should fit easily.