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
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
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
If it's older - please refer to the MQTT HVAC config guide to adjust needed configuration.
- Edit Home Assistant main configuration file configuration.yaml.
- Add the contents 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.
mqtt:
climate:
- name: "My Thermostat"
unique_id: "my_thermostat_1"
modes:
- "off"
- "heat"
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
sensor:
- name: "Current temperature"
state_topic: "opentherm-thermostat/current-temperature/get"
value_template: "{{ value }}"
unit_of_measurement: '°C'
icon: mdi:thermometer
- name: "Boiler temperature"
state_topic: "opentherm-thermostat/boiler-temperature/get"
value_template: "{{ value }}"
unit_of_measurement: '°C'
icon: mdi:thermometer
- name: "Boiler target temperature"
state_topic: "opentherm-thermostat/boiler-target-temperature/get"
value_template: "{{ value }}"
unit_of_measurement: '°C'
icon: mdi:thermometer
Put all things together
- Use 2-pin screw terminal to connect ESP8266 Thermostat Shield to the boiler.
- Connect micro-USB cable
- Go to the Home Assistant UI