Water heater / Boiler: Temperature display & control with OpenHAB rules, MySensors sensor/relay, MQTT

Hi all

I wanted to share my setup in order to help others who want to display hot water temperature and control/schedule a water heater to heat water to some predefined temperatures.

The benefit of displaying water temperature is obvious. But also being able to heat water to predefined temperatures has some significant advantages over time based control, especially in terms of electricity savings. Even more so when combined with a solar water heater like mine.

So, before we begin, let’s see it in action:

Now, first things first, if not already installed, you’ll need to install the MQTT Thing binding as well as Mosquitto.

Next you’ll need two .things files. One is for the broker, with the following contents:

mqtt:broker:mosquittoBroker [ host="127.0.0.1", secure=false, username="user", password="pass" ]

Username and password don’t need to be set if you haven’t set them up on Mosquitto.

The other .things file is the bridge:

Bridge mqtt:broker:mosquittoBroker [ host="127.0.0.1", secure=false ]
{
    Thing topic waterHeaterThermometer {
    Channels:
        Type number : temperature "Water Heater Temperature" [ stateTopic="mysensors-out/2/0/1/0/0" ]
        Type switch : relay  "Water Heater Relay" [ commandTopic="mysensors-in/2/1/1/0/2" ]
    }
}

The first number in the topics is the node ID of your MySensors node. If “2” is already taken in your network, change this here and also in the node sketch below.

Next, you’ll need to create 4 items (I created them in Paper UI):
EW_WaterHeater_Temperature - Number
EW_WaterHeater_Relay - Switch
EW_WaterHeater_Boost_1x - Switch
EW_WaterHeater_Boost_2x - Switch

To speak with your node, you need to have a MySensors MQTT gateway (with topic names “mysensors-in” and “mysensors-out”). I won’t cover this here since it’s already covered on mysensors.org and depends on the hardware you want to use.

For the node, I’ve used an Arduino Nano connected to an RFM69W radio (instructions also on mysensors.org) as well as:

  • A 10K NTC thermistor in series with a 10K resistance, connected between 3.3V and GND, with the middle part connected to pin A1 (similar to this but with an Arduino Nano and pin A1 instead of A0).
  • The thermistor obviously has to be placed somewhere inside the boiler. Depending on your water heater / boiler type, there might be different solutions to this. I used a shielded CAT6 UTP cable to connect the thermistor to the Arduino since my water heater is on the roof.
  • Shorted 3.3V output with AREF, as in the link above
  • Pin D3 connected to an optocoupler relay like this one, able to control 220V.
  • The relay controls a 220V channel which in turn controls a 25A 2NO power contactor, which in turn controls the water heater’s resistance. I used a Legrand contactor which also has a manual override switch. I thought this would be useful in case my system was down.

This is the completed node (the optocoupler is inside the electric panel):

This is the sketch for the node:

#define THERMISTORPIN A1         // which analog pin to connect
#define THERMISTORNOMINAL 10000      // resistance at 25 degrees C
#define TEMPERATURENOMINAL 25   // temp. for nominal resistance (almost always 25 C)
#define NUMSAMPLES 5        // how many samples to take and average, more takes longer but is more 'smooth'
#define BCOEFFICIENT 3950     // The beta coefficient of the thermistor (usually 3000-4000)
#define SERIESRESISTOR 10000    // the value of the 'other' resistor
//#define MY_DEBUG    // Enable debug prints to serial monitor

#define MY_NODE_ID 2

#define RELAY_PIN 3

//#define MY_DEBUG  // Enable debug prints to serial monitor

// Enable and select radio type attached
#define MY_RADIO_RFM69
#define MY_RFM69_FREQUENCY RFM69_868MHZ
#define MY_RFM69_NEW_DRIVER
//#define MY_DEBUG_VERBOSE_RFM69


#include <SPI.h>
#include <MySensors.h>  

#define COMPARE_TEMP 1 // Send temperature only if changed? 1 = Yes 0 = No

unsigned long SLEEP_TIME = 12000; // Wait time between reads (in milliseconds)

int lastTemperature;
bool receivedConfig = false;
bool metric = true;
// Initialize temperature message
MyMessage msg(0,V_TEMP);

uint16_t samples[NUMSAMPLES];

void presentation() {
  // Send the sketch version information to the gateway and Controller
  sendSketchInfo("Temperature Sensor", "1.1");

  present(0, S_TEMP); //Temperature sensor
  present(1, S_BINARY); //Relay controller
  
}

void before()
{
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);
}
 
void setup(void) {
  // connect AREF to 3.3V and use that as VCC, less noisy!
  analogReference(EXTERNAL);
  
}
 
void loop(void) {
  uint8_t i;
  float average;
 
  // take N samples in a row, with a slight delay
  for (i=0; i< NUMSAMPLES; i++) {
   samples[i] = analogRead(THERMISTORPIN);
   delay(10);
  }
 
  // average all the samples out
  average = 0;
  for (i=0; i< NUMSAMPLES; i++) {
     average += samples[i];
  }
  average /= NUMSAMPLES;
 
  Serial.print("Average analog reading "); 
  Serial.println(average);
 
  // convert the value to resistance
  average = 1023 / average - 1;
  average = SERIESRESISTOR / average;
  Serial.print("Thermistor resistance "); 
  Serial.println(average);
 
  float temperature;
  int temp_rounded;
  temperature = average / THERMISTORNOMINAL;     // (R/Ro)
  temperature = log(temperature);                  // ln(R/Ro)
  temperature /= BCOEFFICIENT;                   // 1/B * ln(R/Ro)
  temperature += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
  temperature = 1.0 / temperature;                 // Invert
  temperature -= 273.15;                         // convert to C

  //round temperature to the nearest integer
  temp_rounded = (int) (temperature + 0.5);

  Serial.print("Temperature "); 
  Serial.print(temperature);
  Serial.println(" *C");

  // Only send data if temperature has changed and no error
  if (lastTemperature != temp_rounded && temp_rounded != -127 && temperature != 85) {
    // Send in the new temperature
    send(msg.setSensor(0).set(temperature,0));
    // Save new temperatures for next compare
    lastTemperature=temp_rounded;
  }
  
  wait(SLEEP_TIME);
}

void receive(const MyMessage &message)
{
    // We only expect one type of message from controller. But we better check anyway.
    if (message.type==V_STATUS) {
        // Change relay state
        digitalWrite(RELAY_PIN, message.getBool()?true:false);
        
        // Write some debug info
        Serial.print("Incoming change for sensor:");
        Serial.print(message.sensor);
        Serial.print(", New status: ");
        Serial.println(message.getBool());
    }
}

The next part is the rules. They work under the following assumptions:

  • There are two predefined target temperatures, one lower and one higher. Make sure the higher one is lower than your boiler’s internal thermostat setting, otherwise it will never be reached!
  • There is a failsafe that turns the boiler off after two hours, even if temperature has not been reached.
  • My thermistor is being affected by the electrical resistance of the boiler. Therefore when it’s on, the readings are higher than the water’s average temperature, as the water is locally much hotter. This is why I’m using timers to switch off for a while, take a reading (after it cools down a bit) and then switch on again if needed. But if your thermistor doesn’t have the same issue, this can be simplified a lot. Also the timings might need to be adjusted to fit your needs.
  • There is also a rule to turn on the boiler on workday mornings, set to the lower temperature. You can adjust or delete this as needed.

Here are the rules:

val boost1xLimit = 42
val boost2xLimit = 59
var curLimit = 0

var Timer failSafeTimer
var Timer delayTimer
var Timer coolDownTimer

rule "Water heater Temperature Change"
when
    Item EW_WaterHeater_Temperature changed
then
    logDebug("waterheater", "Temperature change: start")
    var Number temp = EW_WaterHeater_Temperature.state as DecimalType

    if (curLimit > 0 && temp >= curLimit) {
        logDebug("waterheater", "Temperature change: Temperature target " + curLimit + "C reached")
        if (delayTimer !== null || coolDownTimer !== null)
        {
            logDebug("waterheater", "Temperature change: delayTimer/coolDownTimer already running")
            return;
        }
        logDebug("waterheater", "Temperature change: Creating delayTimer")
        delayTimer = createTimer(now.plusMinutes(8), [ |
            logDebug("waterheater", "Temperature change: Entered delayTimer")
            EW_WaterHeater_Relay.sendCommand(OFF)
            logDebug("waterheater", "Temperature change: Creating coolDownTimer")
            coolDownTimer = createTimer(now.plusMinutes(2), [ |
                logDebug("waterheater", "Temperature change: Entered coolDownTimer")
                temp = EW_WaterHeater_Temperature.state as DecimalType
                logDebug("waterheater", "Temperature change: temp is " + temp)
                if (temp >= curLimit) {
                    logDebug("waterheater", "Temperature change: Temp reached, shutting down")
                    EW_WaterHeater_Boost_1x.sendCommand(OFF)
                    EW_WaterHeater_Boost_2x.sendCommand(OFF)
                } else {
                    logDebug("waterheater", "Temperature change: Not reached, reopen relay")
                    EW_WaterHeater_Relay.sendCommand(ON)
                }
                coolDownTimer = null
            ])
            delayTimer = null
        ])

    }

end

rule "Water heater Boost Button Press"
when
    Item EW_WaterHeater_Boost_1x received command
    or Item EW_WaterHeater_Boost_2x received command
then
    logDebug("waterheater", "Boost: start")
    failSafeTimer?.cancel
    failSafeTimer = null
    delayTimer?.cancel
    delayTimer = null
    coolDownTimer?.cancel
    coolDownTimer = null
    if (triggeringItem.name == 'EW_WaterHeater_Boost_1x') {
        curLimit = boost1xLimit
        EW_WaterHeater_Boost_2x.postUpdate(OFF)
    } else {
        curLimit = boost2xLimit
        EW_WaterHeater_Boost_1x.postUpdate(OFF)
    }
    if (receivedCommand == ON && (EW_WaterHeater_Temperature.state as DecimalType) < curLimit) {
        logDebug("waterheater", "Boost: Turning on. Target temperature = " + curLimit + "C")
        EW_WaterHeater_Relay.sendCommand(ON)
        failSafeTimer = createTimer(now.plusMinutes(120), [ |
            logDebug("waterheater", "Boost: Entered failsafe timer")
            triggeringItem.sendCommand(OFF)
        ])
    } else {
        logDebug("waterheater", "Boost: Turning off")
        triggeringItem.postUpdate(OFF)
        EW_WaterHeater_Relay.sendCommand(OFF)
        curLimit = 0
    }
end


rule "Water heater Morning Boost"
when
    //every weekday at 07:30
    Time cron "0 5 7 ? * MON-FRI *"
then
    logDebug("waterheater", "Morning boost: start")
    EW_WaterHeater_Boost_1x.sendCommand(ON)
end

There are two limitations with the above:

  • The failsafe only works if you use the boost switches, not if you turn on the relay item directly.
  • There is no check (either at the mysensors level or the rule level) for a failed signal, i.e. if you send a switch on/off signal but it doesn’t reach the node. I will probably add this at some point.

I think that’s it. If I’m forgetting a step, please let me know. Also, any suggestions are obviously welcome!

6 Likes