Home Assistant OpenTherm Thermostat
Details
  • 12/8/2020
  • OpenTherm, Home Assistant
Share

Home Assistant OpenTherm Thermostat

This article describes how to build and automate a simple OpenTherm Wi-Fi thermostat using ESP8266 Thermostat Shield. So you can integrate your boiler control into your home automation system.

What is Home Assistant?

Home Assistant is an open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.

Home Assistant environment setup


Please refer to the official documentation on how to install and make initial configuration of your instance. Also you'll need MQTT broker and MQTT integration installed and configured within Home Assistant. If you have no broker yet, you can install it with a few steps:

  • Go to 'Supervisor' section in the left pane
  • Select 'Add-on Store' tab
  • Click on 'Mosquitto broker' item and install it
  • Once installed go to 'Configuration' tab and specify some username and password
  • Back to the 'Info' tab and start the add-on


Home Assistant Mosquitto Configuration Home Assistant Mosquitto Broker Installation Home Assistant Mosquitto Broker Configuration Home Assistant Mosquitto Broker Start Up

To configure MQTT integration:

  • Go to 'Configuration' section in the left pane
  • Select 'Integrations' item
  • Click 'Add Integrations' btn
  • Search and install 'MQTT' integration
  • Specify your broker address, user, password and submit changes


Home Assistant MQTT Integration Home Assistant MQTT Integration Search Home Assistant MQTT Integration Configuration

Arduino IDE libraries installation


  • Install Ihor Melnyk's OpenTherm library
  • Install DallasTemperature and OneWire library
  • Install PubSubClient library

In case you forgot, or don't know how to install Arduino libraries click here.

Home Assistant OpenTherm Thermostat Sketch


  • Create new sketch in Arduino IDE and copy code below.
  • Specify WI-FI SSID, WI-FI password, MQTT broker address and update pins configuration if you want to use ESP32 module.
  • Connect WeMos D1 Mini or WeMos D1 Mini ESP32 module and upload updated sketch.


/*************************************************************
  This example runs directly on ESP8266 chip.

  Please be sure to select the right ESP8266 module
  in the Tools -> Board -> WeMos D1 Mini

  Adjust settings in Config.h before run
 *************************************************************/

#include <ESP8266WiFi.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <PubSubClient.h>
#include <OpenTherm.h>

// Your WiFi credentials.
// Set password to "" for open networks.
const char* ssid = "YOUR_WIFI";
const char* pass = "YOUR_PASSWORD";

// Your MQTT broker address and credentials
const char* mqtt_server = "xxx.xxx.xxx.xxx";
const char* mqtt_user = "mqtt_user";
const char* mqtt_password = "mqtt_password";
const int mqtt_port = 1883;

// Master OpenTherm Shield pins configuration
const int OT_IN_PIN = 4;  //for Arduino, 4 for ESP8266 (D2), 21 for ESP32
const int OT_OUT_PIN = 5; //for Arduino, 5 for ESP8266 (D1), 22 for ESP32

// Temperature sensor pin
const int ROOM_TEMP_SENSOR_PIN = 14; //for Arduino, 14 for ESP8266 (D5), 18 for ESP32

// MQTT topics
const char* CURRENT_TEMP_GET_TOPIC = "opentherm-thermostat/current-temperature/get";
const char* CURRENT_TEMP_SET_TOPIC = "opentherm-thermostat/current-temperature/set";
const char* TEMP_SETPOINT_GET_TOPIC = "opentherm-thermostat/setpoint-temperature/get";
const char* TEMP_SETPOINT_SET_TOPIC = "opentherm-thermostat/setpoint-temperature/set";
const char* MODE_GET_TOPIC = "opentherm-thermostat/mode/get";
const char* MODE_SET_TOPIC = "opentherm-thermostat/mode/set";
const char* TEMP_BOILER_GET_TOPIC = "opentherm-thermostat/boiler-temperature/get";
const char* TEMP_BOILER_TARGET_GET_TOPIC = "opentherm-thermostat/boiler-target-temperature/get";


const unsigned long extTempTimeout_ms = 60 * 1000;
const unsigned long statusUpdateInterval_ms = 1000;

float  sp = 15, //set point
       t = 15, //current temperature
       t_last = 0, //prior temperature
       ierr = 25, //integral error
       dt = 0, //time between measurements
       op = 0; //PID controller output
unsigned long ts = 0, new_ts = 0; //timestamp
unsigned long lastUpdate = 0;
unsigned long lastTempSet = 0;

bool heatingEnabled = true;

#define MSG_BUFFER_SIZE  (50)
char msg[MSG_BUFFER_SIZE];

OneWire oneWire(ROOM_TEMP_SENSOR_PIN);
DallasTemperature sensors(&oneWire);
OpenTherm ot(OT_IN_PIN, OT_OUT_PIN);
WiFiClient espClient;
PubSubClient client(espClient);

void ICACHE_RAM_ATTR handleInterrupt() {
  ot.handleInterrupt();
}

float getTemp() {
  unsigned long now = millis();
  if (now - lastTempSet > extTempTimeout_ms)
    return sensors.getTempCByIndex(0);
  else
    return t;
}

float pid(float sp, float pv, float pv_last, float& ierr, float dt) {
  float KP = 10;
  float KI = 0.02;
  // upper and lower bounds on heater level
  float ophi = 63;
  float oplo = 20;
  // calculate the error
  float error = sp - pv;
  // calculate the integral error
  ierr = ierr + KI * error * dt;
  // calculate the measurement derivative
  //float dpv = (pv - pv_last) / dt;
  // calculate the PID output
  float P = KP * error; //proportional contribution
  float I = ierr; //integral contribution
  float op = P + I;
  // implement anti-reset windup
  if ((op < oplo) || (op > ophi)) {
    I = I - KI * error * dt;
    // clip output
    op = max(oplo, min(ophi, op));
  }
  ierr = I;

  Serial.println("sp=" + String(sp) + " pv=" + String(pv) + " dt=" + String(dt) + " op=" + String(op) + " P=" + String(P) + " I=" + String(I));

  return op;
}

// This function calculates temperature and sends data to MQTT every second.
void updateData()
{
  //Set/Get Boiler Status
  bool enableHotWater = true;
  bool enableCooling = false;
  unsigned long response = ot.setBoilerStatus(heatingEnabled, enableHotWater, enableCooling);
  OpenThermResponseStatus responseStatus = ot.getLastResponseStatus();
  if (responseStatus != OpenThermResponseStatus::SUCCESS) {
    Serial.println("Error: Invalid boiler response " + String(response, HEX));
  }

  t = getTemp();
  new_ts = millis();
  dt = (new_ts - ts) / 1000.0;
  ts = new_ts;
  if (responseStatus == OpenThermResponseStatus::SUCCESS) {
    op = pid(sp, t, t_last, ierr, dt);
    ot.setBoilerTemperature(op);
  }
  t_last = t;
  sensors.requestTemperatures(); //async temperature request

  snprintf (msg, MSG_BUFFER_SIZE, "%s", String(op).c_str());
  client.publish(TEMP_BOILER_TARGET_GET_TOPIC, msg);

  snprintf (msg, MSG_BUFFER_SIZE, "%s", String(t).c_str());
  client.publish(CURRENT_TEMP_GET_TOPIC, msg);

  float bt = ot.getBoilerTemperature();
  snprintf (msg, MSG_BUFFER_SIZE, "%s", String(bt).c_str());
  client.publish(TEMP_BOILER_GET_TOPIC, msg);

  snprintf (msg, MSG_BUFFER_SIZE, "%s", String(sp).c_str());
  client.publish(TEMP_SETPOINT_GET_TOPIC, msg);

  snprintf (msg, MSG_BUFFER_SIZE, "%s", heatingEnabled ? "heat" : "off");
  client.publish(MODE_GET_TOPIC, msg);

  Serial.print("Current temperature: " + String(t) + " °C ");
  String tempSource = (millis() - lastTempSet > extTempTimeout_ms)
                      ? "(internal sensor)"
                      : "(external sensor)";
  Serial.println(tempSource);
}

String convertPayloadToStr(byte* payload, unsigned int length) {
  char s[length + 1];
  s[length] = 0;
  for (int i = 0; i < length; ++i)
    s[i] = payload[i];
  String tempRequestStr(s);
  return tempRequestStr;
}

const String setpointSetTopic(TEMP_SETPOINT_SET_TOPIC);
const String currentTempSetTopic(CURRENT_TEMP_SET_TOPIC);
const String modeSetTopic(MODE_SET_TOPIC);

void callback(char* topic, byte* payload, unsigned int length) {
  const String topicStr(topic);

  String payloadStr = convertPayloadToStr(payload, length);

  if (topicStr == setpointSetTopic) {
    Serial.println("Set target temperature: " + payloadStr);
    sp = payloadStr.toFloat();
  }
  else if (topicStr == currentTempSetTopic) {
    t = payloadStr.toFloat();
    lastTempSet = millis();
  }
  else if (topicStr == modeSetTopic) {
    Serial.println("Set mode: " + payloadStr);
    if (payloadStr == "heat")
      heatingEnabled = true;
    else if (payloadStr == "off")
      heatingEnabled = false;
    else
      Serial.println("Unknown mode");
  }
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    const char* clientId = "opentherm-thermostat-test";
    if (client.connect(clientId, mqtt_user, mqtt_password)) {
      Serial.println("ok");

      client.subscribe(TEMP_SETPOINT_SET_TOPIC);
      client.subscribe(MODE_SET_TOPIC);
      client.subscribe(CURRENT_TEMP_SET_TOPIC);
    } else {
      Serial.print(" failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup()
{
  Serial.begin(115200);

  Serial.println("Connecting to " + String(ssid));
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);

  int deadCounter = 20;
  while (WiFi.status() != WL_CONNECTED && deadCounter-- > 0) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Failed to connect to " + String(ssid));
    while (true);
  }
  else {
    Serial.println("ok");
  }

  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);

  ot.begin(handleInterrupt);

  //Init DS18B20 sensor
  sensors.begin();
  sensors.requestTemperatures();
  sensors.setWaitForConversion(false); //switch to async mode
  t, t_last = sensors.getTempCByIndex(0);
  ts = millis();
  lastTempSet = -extTempTimeout_ms;
}

void loop()
{
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  unsigned long now = millis();
  if (now - lastUpdate > statusUpdateInterval_ms) {
    lastUpdate = now;
    updateData();
  }
}

Home Assistant Configuration


  • Edit Home Assistant main configuration file configuration.yaml.
  • Replace all the contents with provided below.
  • Restart your Home Assistant instance.
  • Edit dashboard.
  • Add card by entity name 'Test OT'.
  • Optional: Also you can add 'entities' and history cards to reflect boiler setpoint and current temperature.


# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:

sensor:
  - platform: mqtt
    name: "Current temperature"
    state_topic: "opentherm-thermostat/current-temperature/get"
    value_template: "{{ value }}"
    unit_of_measurement: '°C'
    icon: mdi:thermometer
  - platform: mqtt
    name: "Boiler temperature"
    state_topic: "opentherm-thermostat/boiler-temperature/get"
    value_template: "{{ value }}"
    unit_of_measurement: '°C'
    icon: mdi:thermometer
  - platform: mqtt
    name: "Boiler target temperature"
    state_topic: "opentherm-thermostat/boiler-target-temperature/get"
    value_template: "{{ value }}"
    unit_of_measurement: '°C'
    icon: mdi:thermometer

climate:
  - platform: mqtt
    modes:
      - "off"
      - "heat"
    name: "Test OT"
    current_temperature_topic: "opentherm-thermostat/current-temperature/get"
    mode_command_topic: "opentherm-thermostat/mode/set"
    mode_state_topic: "opentherm-thermostat/mode/get"
    temperature_command_topic: "opentherm-thermostat/setpoint-temperature/set"
    temperature_state_topic: "opentherm-thermostat/setpoint-temperature/get"
    min_temp: 12
    max_temp: 28
    value_template: "{{ value }}"
    temp_step: 0.5

group: !include groups.yaml
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml


Home Assistant Thermostat Card Search Home Assistant Thermostat Card Configuration

Put all things together

Master OpenTherm Shield Connection Home Assistant OpenTherm Thermostat Shield

  • Use 2-pin screw terminal to connect ESP8266 Thermostat Shield to the boiler.
  • Connect micro-USB cable
  • Go to the Home Assistant UI

Home Assistant thermostat card in action

Home Assistant OpenTherm Thermostat Card

Home Assistant mobile app

Also you can download Home Assistant android app (Play Store)


Home Assistant OpenTherm Thermostat Mobile Application