DEV Community

Mikk Mangus
Mikk Mangus

Posted on

Steady room temperature with node-red

Most of the rooms at my home are heated using electric radiators. The radiators have a thermostat built in and a dial that controls the temperature.

But the problem is that this dial mostly controls the temperature of the radiator itself, not the room. Without touching the dial, the temperature in the room fluctuates about ±1°C every 24h. Moreover, if it gets colder or warmer outside, the dial needs to be adjusted accordingly to keep the warmth in the room temperate.

This is an article on how I achieved a steady temperature at home.

The hardware

As I was already running zigbee2mqtt and Node-RED on a Rasberry to control some automations I have set up at home, I chose to use the same stack. I already had different Zigbee temperature sensors installed to each room and the information about the temperature available.

Bought a pack of Tuya Zigbee relays that are rated at 4000W, which is plenty for my heaters that use 1250W. The relays cost about $10 each.
Tuya Zigbee relay

The relays got installed in series to the radiators meaning the only function they provide is to cutoff the power to the radiator.

This is how it looked like with the radiator off the wall and the cover of the connections-box removed (sorry about the dust):
The power connections
And this is how it looks with the relay in series:
The result

The software

The Simplest is not always the best

The initial algorithm I tried for controlling the temperature was the simplest imaginable: cut off the power once the temperature reaches the setpoint, otherwise keep the power on. This unfortunately did not make the temperature any steadier. As it usually takes an hour to raise the temperature about 1°C, once the power was cut off, the temperature was overshooting more than 1°C from the setpoint and then 3 hours later undershooting more than 1°C.

That clearly wasn't good enough, it was even worse than without the relays 😞

I have worked quite a lot with different PID setups, but this time I felt a truly PID algorithm was hard to put to use because you need to factor in the position of the dial on the radiator as well as the update rate of the zigbee temperature sensors.

The second iteration

With the second iteration, I introduced another piece of state for the heater in my software, which I call onPercentage. As the name suggests, it is the percentage of the time the heater should be on. Instead of keeping the heater on all the time, it now is turned on and off every 10 minutes according to the onPercentage as follows:

if (temperature < setPoint) {
    const minute = parseInt(time.split(':')[1]);

    if (minute % 10 >= onPercentage / 10) {
        return off();

    // to avoid turning all heaters on at the same moment
    await (new Promise(resolve => setTimeout(resolve, Math.random() * 5000)));

    return on();
Enter fullscreen mode Exit fullscreen mode

As you can see from the code, I also added some randomness when turning on the heaters, because there's always a momentary spike in load whenever switching on an electrical device, and as the algorithm switches on all the heaters at once when minute % 10 === 0, I felt it might become necessary.

That's only half of the algorithm, the onPercentage itself is adjusted by the overshooting and undershooting of the temperature reading. If the temperature is still increasing while the setpoint has been reached, the onPercentage is lowered by 10% and vice versa. This is how it looks in the code:

const room =;
const onPercentage = global.get(`${room}.heater_on_percentage`) || 50;
const setPoint = global.get(`${room}.temperature_setpoint`) || 21;

const previous = global.get(`${room}.temperature`);
const current = payload.temperature;
const increasing = current > previous;
const decreasing = current < previous;

if (setPoint < previous && increasing && onPercentage > 20) {
    return onPercentage - 10;
else if (setPoint > previous && decreasing && onPercentage < 100) {
    return onPercentage + 10;
// not returning anything means the previous value is retained
Enter fullscreen mode Exit fullscreen mode

Essentially it is like a simplified version of using only the proportional (turned on when setpoint not reached) and integral (onPercentage controlled by overshooting) parts of the PID.

Anyways, when I had the onPercentage implemented, I was amazed how well it turned out! It has now been a few months since I made this change and I have not touched the code after the first deploy 🎉. The temperature has been rock steady next to the setpoint throughout every day even with the temperature outside changing. I have set the dial on the radiator a little higher than before to allow it reach the setpoint easily. I have only adjusted some of the dials once when it got a lot colder outside and the onPercentage maxxed out.

An added benefit is that I can now control the heating/temperature programmatically and/or remotely.

A sight to my node-red flow that does the temperature controlling:
Controlling temperature with node-red

If you happen to be more familiar with Node-RED, you might notice the function nodes without an input and the blue nodes on the right bottom of the screen -- yes, those are custom built nodes for modifying the state and subscribing to the state changes in a "React hooks"-like way.
I might release that node-red-contribution soon™️, but need to clean the implementation up a little before I do 🙂

Have fun!

Top comments (1)

murjam profile image
Mikk Mangus

Although the state management node is released by my wife, I was part of it and therefore feel I did keep my promise of making it available for everyone one day 😄