Steady room temperature with node-red
Tags: nodered, zigbee, zigbee2mqtt, homeautomation
Published: 2021-01-17 11:16:38

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](https://www.zigbee2mqtt.io/) and [Node-RED](https://nodered.org/) 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](https://dev-to-uploads.s3.amazonaws.com/i/5i4qd5mtp0xbv64ieong.jpg)

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](https://dev-to-uploads.s3.amazonaws.com/i/eeb6il1n3pbiejldolgj.jpg)
And this is how it looks with the relay in series:
![The result](https://dev-to-uploads.s3.amazonaws.com/i/gnlg15nkadplu3206el7.jpg)

## 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 :disappointed:

I have worked quite a lot with different [PID](https://en.wikipedia.org/wiki/PID_controller) 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:
```js
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();
}
```
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:
```js
const room = msg.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
```
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](https://en.wikipedia.org/wiki/PID_controller).

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 :tada:. 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](https://dev-to-uploads.s3.amazonaws.com/i/nktiicso3e73uagt2vks.jpg)

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:tm:, but need to clean the implementation up a little before I do :slightly_smiling_face:

Have fun!