Safe heating with MySensors - how I use it as a smart thermostat



  • After reading the thread about the lovely PiHome thermostat I thought I'd share how I am heating my home.

    I'm currently using Domoticz with the Smart Virtual Thermostat plugin. This is a pure software PID controller designed to heat homes with on-off devices like electric heaters.

    My safe heater switch node

    Using MySensors I created a special 'secure heating switch' with a temperature sensor, gas sensor, and a relay. It has all kinds of internal safety checks:

    • is the room not too hot?
    • has the heater not been on too long?
    • is the temperature sensor still giving sane data?
    • is the controller still connected?
    • is there a gas leak?

    It also has an analog 'backup': if the room temperature gets above 25 degrees celcius a simple bi-metal cuts power to the heater.

    Other features

    • You can set the minimum time that the switch must stay in a certain position. This protects mechanical valves in some heaters from wear and other problems.
    • Non-blocking. So it can act as a repeater too.
    • LED indicates if there is a problem.

    HARDWARE

    The node is designed to be as plug and play as possible, and doesn't really require any soldering. I just connect a Dallas sensor and a relay switch to the 'base node':

    alt text

    CODE

    
    /**
     * 
     * The MySensors Arduino library handles the wireless radio link and protocol
     * between your home built sensors/actuators and HA controller of choice.
     * The sensors forms a self healing radio network with optional repeaters. Each
     * repeater and gateway builds a routing tables in EEPROM which keeps track of the
     * network topology allowing messages to be routed to nodes.
     *
     * Created by Henrik Ekblad <henrik.ekblad@mysensors.org>
     * Copyright (C) 2013-2015 Sensnology AB
     * Full contributor list: https://github.com/mysensors/Arduino/graphs/contributors
     *
     * Documentation: http://www.mysensors.org
     * Support Forum: http://forum.mysensors.org
     *
     * This program is free software; you can redistribute it and/or
     * modify it under the terms of the GNU General Public License
     * version 2 as published by the Free Software Foundation.
     *
     *******************************
     *
     * DESCRIPTION
     * 
     * SAFE HEATER SWITCH WITH TEMPERATURE SENSOR
     *
     * A 'safe' switch for heaters that can be controlled via an external controller. The heater is switched off..
     * - At boot
     * - When connection to the controller has been lost (it creates a text-child which it then keeps requesting back)
     * - When the built-in temperature sensor detects that the room is too warm.
     * - When the temperature sensor seems broken
     * - When the heater has been on for too long (this starts a cooldown period).
     * 
     * Other features
     * - You can set the minimum time that the switch must stay in a certain position. This protects mechanical valves in some heaters from wear and other problems.
     * - Non-blocking. So it can act as a relay too.
     * - LED indicates if there is a problem.
     * 
     *
     * IMPORTANT: 
     * - I use this in combination with an analog bi-metal safety shutoff (which turns of power to the heater if temperature gets too high).
     * - My heater is a gas-model that will automatically turn off when electricity fails. MAke sure your heater fails gracefully when power is turned off!
     * 
     * Possible improvements:
     * - Add more temperature sensors to better check if the current temprature is accurate.
     * - Add a display.
     * - This could be expanded into being a thermostat/PID itself.
     */
    
    
    // if you uncomment this, you can get test and debug updates about everything the sensor is doing by using the serial monitor tool.
    #define MY_DEBUG
    
    // advanced options
    //#define MY_NODE_ID 13                             
    //#define MY_PARENT_NODE_ID 0
    //#define MY_PARENT_NODE_IS_STATIC
    #define MY_TRANSPORT_WAIT_READY_MS 10000            // try connecting for 10 seconds. Otherwise just continue.
    #define MY_SPLASH_SCREEN_DISABLED                   // saves a little memory.
    //#define MY_DISABLE_RAM_ROUTING_TABLE_FEATURE      // saves a little memory.
    //#define MY_RF24_CHANNEL 100                       // in EU the default channel 76 overlaps with wifi, so you may want to make it higher. But this wil then need to be done everywhere, including on your gateway.
    //#define MY_RF24_DATARATE RF24_1MBPS               // a slower datarate could make the network more stable?
    
    // Easy to use security, yay!
    //#define MY_SIGNING_SIMPLE_PASSWD   "putyourpasswordhere"
    //#define MY_SIGNING_SOFT_RANDOMSEED_PIN A7         // setting a pin to pickup noise makes encryption more secure.
    
    // Enable and select radio type attached
    #define MY_RADIO_NRF24
    //#define MY_RADIO_NRF5_ESB
    //#define MY_RADIO_RFM69
    //#define MY_RADIO_RFM95
    
    // Set LOW transmit power level as default, if you have an amplified NRF-module and
    // power your radio separately with a good regulator you can turn up PA level. Choose one:
    //#define MY_RF24_PA_LEVEL RF24_PA_MIN
    #define MY_RF24_PA_LEVEL RF24_PA_LOW
    //#define MY_RF24_PA_LEVEL RF24_PA_HIGH
    //#define MY_RF24_PA_LEVEL RF24_PA_MAX
    
    // Enable repeater functionality for this node. It is designed to be "non-blocking".
    #define MY_REPEATER_FEATURE
    
    // LIBRARIES (in the Arduino IDE go to Sketch -> Include Library -> Manage Libraries to add these if you don't have them installed yet.)
    #include <MySensors.h>
    #include <DallasTemperature.h>
    #include <OneWire.h>
    
    
    // VARIABLES YOU CAN CHANGE
    #define SAFEGUARD_TEMP 24                         // Maximum temperature (currently in celcius) that the sensor may detect before the relay is shutdown.
    #define RETURNTONORMAL_TEMP 22                    // After a safeguard shutdown, at what cooled down temperature (currently in celcius) should the system go back to normal state?
    #define COMPARE_TEMP 0                            // Send temperature only if changed? 1 = Yes 0 = No.
    #define ONE_WIRE_BUS 3                            // Digital pin where Dallas sensor(s) is/are connected.
    #define MAXATTACHEDDS18B20 1                      // Maximum amount of teperature sensors you can connect to this arduino (can be 16).
    #define REDLED 8                                  // Red LED, useful as alarm colour.
    #define MEASUREMENTINTERVAL 60000                 // Time to wait between reads (in milliseconds). (60 seconds)
    #define CONNECTIONCHECKINTERVAL 60000             // Time to wait between checking the connection to the gateway (in milliseconds). (60 seconds)
    #define MANIMUMCONNECTIONLOSSDURATION 181000      // Maximum time that the server can be disconnected (in milliseconds). (181 seconds)
    #define TEMPTHRESHOLD 0.1                         // How big a temperature difference has to be before an update is sent. Makes the sensor less precise, but also less jittery, and can save battery.
    #define MAXIMUMHEATERONDURATION 3600000           // If the heater has been on non-stop for an hour..
    #define HEATERCOORLDOWNDURATION 1200000           // ..cool down for 20 minutes
    #define MIMIMUMDELAYBETWEENSWITCHING 10000        // At most switch the heater state once every X seconds. This protects wear on fireplaces with mechanical switches. (10000 = 10 seconds)
    #define GASDETECTORPIN A0                         // analog pin of gas detector
    #define GASTHRESHOLD 100                          // threshold for LPG detection to cause auto-shutdown, in case of gas leak.
    
    //VARIABLES YOU PROBABLY SHOULDN'T CHANGE
    #define TEMP_CHILD_ID 0                           // for MySensors. Within this node each sensortype should have its own ID number.
    #define RELAY_CHILD_ID 1                          // for MySensors. Within this node each attached thing should have its own ID number.
    #define TEXT_CHILD_ID 2                           // for MySensors. Within this node each sensortype should have its own ID number.
    #define GAS_CHILD_ID 3                           // for MySensors. Within this node each sensortype should have its own ID number.
    
    OneWire oneWire(ONE_WIRE_BUS);                    // Setup a oneWire instance to communicate with any OneWire devices (like Maxim/Dallas temperature ICs)
    DallasTemperature sensors(&oneWire);              // Pass the oneWire reference to Dallas Temperature.
    float lastTemperature[MAXATTACHEDDS18B20];        // creates an array to hold the previous temperature measurements for each possible sensor.
    int numSensors = 0;                               // variable to contain the number of found attached sensors.
    unsigned long measurementSleepTime = 0;           // variable to store the calculated Sleep time if the node is battery powered.
    
    // GAS DETECTOR VARIABLES
    int gasValue = 200;                               // start with a high value, to be safe.
    //#define BUZZERPIN 6                             // digital pin of alarm buzzer
    
    //RELAY VARIABLES
    #define RELAY_1 4  // Arduino Digital I/O pin number for first relay (second on pin+1 etc)
    #define NUMBER_OF_RELAYS 1 // Total number of attached relays
    #define RELAY_ON 1  // GPIO value to write to turn on attached relay
    #define RELAY_OFF 0 // GPIO value to write to turn off attached relay
    float lastMainTemperature = 18; // The temperature when the relay starts. 
    
    
    // Security variables
    int errorCount = 0;                                 // May indicate system stability later.
    unsigned long lastTimeConnected = 0;         // When was the last time the system communicated with the control server?
    unsigned long lastTimeRelaySwitched = 0;     // When was the last time that the relay switched? Used to avoid having the heater on for too long _or_ too short.
    unsigned long heaterOnTooLongTime = 0;       // The moment when the heater had been on for too long. Used to force a cooldown period afterwards.
    boolean safetyShutdown = true;                     // Set to true if something is wrong with the system. This then turns off the relay.
    boolean requestedHeaterStatus = RELAY_OFF;          // At boot, assume the server wants the heater turned off, just to be safe.
    boolean actualHeaterStatus = RELAY_OFF;             // At boot, assume the heater is on, just to be safe.
    boolean gasDetected = false;                        // Does the gas detector smell something?
    boolean heaterOnTooLong = false;                    // If the heater has been on for 60 minutes, it will be shut down for a while.
    boolean roomTooHot = false;                         // If the room is hotter than 24 degrees, disable the heater. (could be runaway heater.. or summertime)
    boolean temperatureSensorError = false;             // Are we getting useful measurements from the temperature sensor?
    boolean serverDisconnected = true;                  // Is the Domoticz server connected? (implying that the gayeway is also working)
    //long timeStamp = 0;                               // current unix time, as received from server. Domoticz doesn't seem to support this currently.
    
    
    // Mysensors settings
    MyMessage tempmsg(TEMP_CHILD_ID,V_TEMP);            // Sets up the message format that we'll be sending to the MySensors gateway later. The first part is the ID of the specific sensor module on this node. The second part tells the gateway what kind of data to expect.
    MyMessage txtmsg(TEXT_CHILD_ID,V_TEXT);             // Sets up the message format that we'll be sending to the MySensors gateway later. The first part is the ID of the specific sensor module on this node. The second part tells the gateway what kind of data to expect.
    MyMessage gasmsg(GAS_CHILD_ID, V_LEVEL);            // Sets up the message format that we'll be sending to the MySensors gateway later. The first part is the ID of the specific sensor module on this node. The second part tells the gateway what kind of data to expect.
    
    void before()
    {
      pinMode(RELAY_1, OUTPUT);
      digitalWrite(RELAY_1, RELAY_OFF);                 // Shut down relay, just to be safe.
      sensors.begin();                                  // Startup up the OneWire library. It allows multiple sensors to talk over one wire (one pin).
    }
    
    
    void setup()
    {
      digitalWrite(RELAY_1, RELAY_OFF);                 // start with the relay off
      pinMode(REDLED, OUTPUT);                          // set LED pin as output pin.
      for(int i=0; i<MAXATTACHEDDS18B20; i++) 
      {
        lastTemperature[i] = 0;  //Pre-filling array with 0's.
      }
      sensors.setWaitForConversion(false); // requestTemperatures() will not block current thread
    
      Serial.begin(115200); // for serial debugging.
      delay(1000);  
      Serial.println(F("Hello world, I am a heater node."));
      request(RELAY_CHILD_ID,V_STATUS);
    }
    
    
    void presentation()
    {
      sendSketchInfo("Secure fireplace relay", "1.1");    // Send the sketch version information to the gateway and Controller
      present(TEMP_CHILD_ID, S_TEMP,"Secure heater temperature");    // Present all sensors modules to the gateway (16 maximum).
      present(RELAY_CHILD_ID, S_BINARY,"Secure heater switch");
      present(TEXT_CHILD_ID, S_INFO,"Secure heater status");
      present(GAS_CHILD_ID, S_AIR_QUALITY, "MQ2 gas sensor");
      send(txtmsg.setSensor(TEXT_CHILD_ID).set( F("Heater switch says hello") ));
    }
    
    
    void loop()
    {
    
      // You should not change these variables:
      static boolean dallasIsMeasuring = true;                  // Used to indicate when the time is right for a new measurement to be made.
      static boolean dallasIsCalculating = false;               // Used to bridge the time that is needed to calculate the temperature values by the Dallas library.
      unsigned long currentMillis = 0;                          // The millisecond clock in the main loop.
      static unsigned long lastSecurityCheck = 0;               // Used to remember the time of the last temperature measurement.
      static unsigned long previousMeasurementMillis = 0;       // Used to remember the time of the last temperature measurement.
      static unsigned long lastPlausibleTemperatureTime = 0;    // Used to remember the time of the last succesful temperature measurement.
      static unsigned long previousMinute = 0;                  // Used to loop once a minute.
      static int16_t conversionTime = 0;                        // Used to store the time needed to calculate the temperature from measurements.
      
      currentMillis = millis(); // The time since the sensor started, counted in milliseconds. This script tries to avoid using the Sleep function, so that it could at the same time be a MySensors repeater.
    
      // Every 10 seconds do a security check.
      if(currentMillis - lastSecurityCheck >= 10000) {
        lastSecurityCheck = currentMillis;
    
        // starts the very short led blink
        digitalWrite(REDLED,HIGH); // alarm led.
    
        // A.
        // CHECKING - Is everything ok?
    
    
        // Is there a gas leak?
        gasValue = analogRead(GASDETECTORPIN);
        Serial.print(F("gas:"));Serial.println(gasValue);
        
        if (gasValue > GASTHRESHOLD) {
          gasDetected = true;
          send(txtmsg.setSensor(TEXT_CHILD_ID).set( F("GAS LEAK!?!") ));
          //tone(BUZZERPIN, 1000, 200);
        }else{
          gasDetected = false;  
        }
    
    
        // Heater on too long?
        if( actualHeaterStatus == RELAY_ON){
          if(currentMillis - lastTimeRelaySwitched >= MAXIMUMHEATERONDURATION){
            heaterOnTooLongTime = currentMillis;
            heaterOnTooLong = true;      
            Serial.println(F("HEATER ON TOO LONG"));
          }
        }
    
        // Is the controller up?
        if(currentMillis - lastTimeConnected >= MANIMUMCONNECTIONLOSSDURATION){
          serverDisconnected = true;
          Serial.println(F("LOST CONNECTION TO CONTROLLER"));
        }else{
          serverDisconnected = false;
        }
    
        // Not too hot in the room?
        if(lastMainTemperature > SAFEGUARD_TEMP){
          roomTooHot = true;
          Serial.println(F("ROOM TOO HOT")); 
        }
        
        // How long has it been since the temperature sensor gave a good measurement?
        if(currentMillis - lastPlausibleTemperatureTime >= MANIMUMCONNECTIONLOSSDURATION){  // Uses same duration as connection loss.
          temperatureSensorError = true;
          Serial.println(F("SHUTDOWN: TEMPERATURE SENSOR ERROR"));
        } else{
          temperatureSensorError = false;
        }
    
        
    
        // B.
        // HEALING - Some functions might return, and then the system could return to normal.
    
        // Heater was too hot, but has had time to cool down.
        if(currentMillis - heaterOnTooLongTime >= HEATERCOORLDOWNDURATION && heaterOnTooLong == true){
          heaterOnTooLong = false;
        }
    
        // Room has cooled down.
        if(roomTooHot == true && lastMainTemperature > RETURNTONORMAL_TEMP){ // For safety, the "return to normal" temperature is lower than the maximum room temperature. This avoids flip-flopping.
          Serial.println(F("THE ROOM HAS NOT COOLED DOWN TO SAFE LEVELS"));
        }else if(roomTooHot == true && lastMainTemperature <= RETURNTONORMAL_TEMP){
          roomTooHot = false;
        }
    
        /*
        // useful debug data:
        Serial.print(F("GasDetected:  "));
        Serial.println(gasDetected);
        Serial.print(F("SensorError:  "));
        Serial.println(temperatureSensorError);
        Serial.print(F("onTooLong:    "));
        Serial.println(heaterOnTooLong);
        Serial.print(F("disconnected: "));
        Serial.println(serverDisconnected);
        Serial.print(F("roomTooHot:   "));
        Serial.println(roomTooHot);
        */
    
        // C.
        // DECIDE IF OPERATION IS SAFE
        if(temperatureSensorError == true || heaterOnTooLong == true || serverDisconnected == true || roomTooHot == true || gasDetected == true){
          Serial.println(F("! ERROR DETECTED"));
          // Decision: something is wrong, so SHUTDOWN the relay.
          if(safetyShutdown == false){
            errorCount++;
            send(txtmsg.setSensor(TEXT_CHILD_ID).set( F("SAFETY SHUTDOWN!") ));
            }
          safetyShutdown = true;
          digitalWrite(RELAY_1, RELAY_OFF); // Turn off heater.
          digitalWrite(REDLED,HIGH); // alarm led.
          Serial.println(F("CURRENTLY IN SAFETY SHUTDOWN MODE"));
        }else{
    
          // Decision: everything looks OK.
          Serial.println(F("NO ERROR DETECTED"));
          if(safetyShutdown == true){
            send(txtmsg.setSensor(TEXT_CHILD_ID).set( F("Heater is OK again!") ));
            digitalWrite(REDLED,LOW); // turn of alarm led.
          }
          safetyShutdown = false;
          Serial.println(F("NORMAL OPERATION"));
          Serial.print(F("act status")); Serial.println(actualHeaterStatus);
          Serial.print(F("req status")); Serial.println(requestedHeaterStatus);
          
          if(requestedHeaterStatus != actualHeaterStatus){
            //One more check to protect the relay from switching too often.
            if(currentMillis - lastTimeRelaySwitched > MIMIMUMDELAYBETWEENSWITCHING){
              
              Serial.print(F("Current switch position: "));
              Serial.println(actualHeaterStatus);
              Serial.print(F("Requested switch position: "));
              Serial.println(requestedHeaterStatus);
              Serial.println(F("NOW SWITCHING THE RELAY"));
              digitalWrite(RELAY_1, requestedHeaterStatus); 
              actualHeaterStatus = requestedHeaterStatus;
              lastTimeRelaySwitched = currentMillis;
              send(txtmsg.setSensor(TEXT_CHILD_ID).set( F("Switching relay") ));
            }else{
              Serial.println(F("Delaying switching the relay"));
              send(txtmsg.setSensor(TEXT_CHILD_ID).set( F("Delaying switching relay") ));
            }
          }
        }
    
    
        // if everything is on, turn off the LED again (making it blink very shortly during the check)
        if(safetyShutdown == false){
          digitalWrite(REDLED,LOW); // alarm led.
        }
      
      }
    
      // Every minute..
      if(currentMillis - previousMinute >= CONNECTIONCHECKINTERVAL) {
        previousMinute = currentMillis;
    
        // check if the network connection is ok by asking the relay state from the controller.
        Serial.println(F("Checking if controller is up and running"));
        request(RELAY_CHILD_ID,V_STATUS);
        //void requestTime(); // Unfortunately Domoticz doesn't seem to support this yet. 
      
        // send the gas value while you're at it.
        send(gasmsg.set(gasValue));
      }
      
    
     // Let's measure the temperature
     if(dallasIsMeasuring == true && currentMillis - previousMeasurementMillis >= MEASUREMENTINTERVAL) { // If we're not calculating, and enough time has passed, we'll start again.
        previousMeasurementMillis = currentMillis; // Mark the time of the initialiation of this measurement.
        dallasIsMeasuring = false; // We're measuring, so let's take it off our to-do list.      
        Serial.println(F("Starting new measurement(s)"));
    
        // Fetch temperatures from Dallas sensors
        sensors.requestTemperatures();
    
        // query conversion time. Apparently it takes a while to calculate.
        //ConversionTime = sensors.millisToWaitForConversion(sensors.getResolution());
        conversionTime = millisToWaitForConversion(sensors.getResolution()); // This is a modified version of the line above, to deal with the problem in the current Dallas library.
        dallasIsCalculating = true; //Next step is to re-calculate the temperature again.
     }
    
    
     // Next, let's calculate the temperature and send the temperature to the gateway if it has changed.
     if(dallasIsCalculating == true && currentMillis - conversionTime > previousMeasurementMillis) {
        dallasIsCalculating = false; // We're doing this now, so check calculating off the to-do list too.
        for (int i=0; i<1 && i<MAXATTACHEDDS18B20; i++){  // Loop through all the attached temperature sensors.   
          float temperature = getControllerConfig().isMetric?sensors.getTempCByIndex(i):sensors.getTempFByIndex(i); // Fetch the temperature form the current sensor
           Serial.print(F("Sensor #"));
           Serial.print(i);
           Serial.print(F(" says it is "));
           Serial.print(temperature);
           Serial.println(F(" degrees"));
    
          if(temperature != -127.00 && temperature != 85.00) { // Avoids working with measurement errors.
            lastPlausibleTemperatureTime = currentMillis;
            lastMainTemperature = temperature;
            
            if (COMPARE_TEMP == 1 && abs(temperature - lastTemperature[i]) < TEMPTHRESHOLD) { // is the temperature difference bigger than the threshold?
               Serial.print(temperature - lastTemperature[i]);
               Serial.println(F("Temperature difference too small"));
            } else {
               Serial.print(temperature - lastTemperature[i]);
               Serial.println(F("Sending the new temperature to the gateway."));
               send(tempmsg.setSensor(i).set(temperature,1));
               lastTemperature[i] = temperature; // Save new temperatures to be able to compare in the next round.
            }
          }
        }
    
        // Both tasks are done. Time to wait until we should measure again.
        Serial.println(F("zzzzZZZZzzzzZZZZzzzz"));
    
        dallasIsMeasuring = true;
      }
      
    }
    
    
    // This function helps to avoid a problem with the latest Dallas temperature library.
    int16_t millisToWaitForConversion(uint8_t bitResolution)
    {
       switch (bitResolution) 
       {
         case 9:
            return 94;
         case 10:
            return 188;
         case 11:
            return 375;
         default:
            return 750;
       }
    }
    
    
    
    void receive(const MyMessage &message)
    {
      Serial.println(F("+++receiving message+++"));
      lastTimeConnected = millis(); // Seems like we are connected to the server, so let's update this variable. Wait, does this work like this?
    
      if (message.type == V_TEXT) {
        Serial.print(F("Received text data: "));
        Serial.println(message.data);
        serverDisconnected = false;
        // Not doing anything with this yet.
      }
    
      
      // We only expect one type of message from controller. But we better check anyway.
      if (message.type==V_STATUS) {
        // Change relay state
    
        requestedHeaterStatus = message.getBool()?RELAY_ON:RELAY_OFF;
        
        // Write some debug info
        Serial.print(F("Incoming change for sensor:"));
        Serial.println(message.sensor);
        Serial.print(F(", New value from controller: "));
        Serial.println(message.getBool());
        Serial.print(F(", New requested Heater Status: "));
        Serial.println(requestedHeaterStatus);
        
      }
    }
    
    
    
    /*
    // currently unused: Domoticz doesn't support it yet.
    void receiveTime(unsigned long timeStamp){
      Serial.print("Time value received: ");
      Serial.println(timeStamp);
      lastTimeConnected = millis();
      //connectionErrorCount = 0;
      };
    */
    
    
    

    QUESTION
    If anyone has a suggestion for an even simpler temperature sensor (with an even smaller library) I'd love to hear it.

    I haven't integrated a boiler, although there are Domoticz plugins for that too.


 

357
Online

7.8k
Users

8.7k
Topics

92.9k
Posts