Domoticz OpenTherm Thermostat
Details
  • 12/30/2020
  • OpenTherm, Domoticz, Thermostat
Share

Domoticz OpenTherm Thermostat

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

What is Domoticz?

Domoticz is a very light weight home automation system that lets you monitor and configure miscellaneous devices, including lights, switches, various sensors/meters like temperature, rainfall, wind, ultraviolet (UV) radiation, electricity usage/production, gas consumption, water consumption and many more. Notifications/alerts can be sent to any mobile device.

Environment setup


Domoticz can be installed on a different devices, please refer to the wiki on how to install and make initial configuration of your instance. For our purposes we will use a windows installation. It is as simple as downloading and running installer wizard similar to most of regular windows apps. Also you'll need MQTT broker installed.

Once you have got Domoticz up and running you'll need to configure MQTT adapter and smart virtual thermostat.

  • Go to your domoticz server web interface (http://127.0.0.1:8083 for our example) and select 'Devices' - 'Hardware' from menu
  • Specify some name, i.e. 'mqtt-client'
  • Select 'MQTT Client Gateway with LAN interface'
  • Specify 'Remote Address', 'Port', 'Username' and 'Password' fields according to your MQTT broker installation
  • Press 'Add' button


Domoticz Main Page Domoticz Main Page Domoticz Main Page


For a Smart Virtual Thermostat plugin installation please refer to the manual page. In our example (windows domoticz installation) please proceed the following steps:

  • Download plugin from git repo (Press green 'Code' button and click 'Download ZIP')
  • Extract downloaded archive contents to 'C:\Program Files (x86)\Domoticz\plugins\' folder A 'C:\Program Files (x86)\Domoticz\' folder might not contain 'plugins', so you have to create it first
  • Rename 'SmartVirtualThermostat-master' folder to 'SVT'
  • Restart domoticz server
  • Go to your domoticz server web interface (http://127.0.0.1:8083 for our example) and select 'Devices' - 'Hardware' from menu
  • Specify some name, i.e. 'smart-thermostat'
  • Select 'Smart Virtual Thermostat'
  • Press 'Add' button
  • Go to Switches page and click on star icon for smart-thermostat Thermostat Control card
  • Repeat step above for smart-thermostat Setpoint Normal card on Utility tab
  • Now please navigate to the 'Setup-Devices' domoticz tab and note an id's created for your sensors, they will be needed for a proper sketch configuration. Especially you'll need smart-thermostat - Setpoint Normal and smart-thermostat - Thermostat Control ones

Domoticz Hardware Page Domoticz MQTT cards configuration
Now you have dashboard configured with cards needed to control your opentherm thermostat

Arduino IDE libraries installation


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

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

Domoticz OpenTherm Thermostat Sketch


  • Create new sketch in Arduino IDE and copy code below.
  • Specify WI-FI SSID, WI-FI password and update pins configuration if you want to use ESP32 module.
  • Replace 'xxx' in SETPOINT_NORMAL_ID, CONTROL_ID with ids of smart-thermostat - Setpoint Normal and smart-thermostat - Thermostat Control respectively
  • Connect WeMos D1 Mini or WeMos D1 Mini ESP32 module and upload updated sketch.

Alternatively you can download sketch from a github repo


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

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

// Your MQTT broker address and credentials
const char* mqtt_server = "xxx.xxx.xxx.xxx";
const char* mqtt_user = "xxxxxx";
const char* mqtt_password = "xxxxxx";
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

const char* OUT_TOPIC = "domoticz/out";
const char* IN_TOPIC = "domoticz/in";

const String SETPOINT_NORMAL_ID = "xxxxxx";
const String CONTROL_ID = "xxxxxx";


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() {
  return sensors.getTempCByIndex(0);
}

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
  Serial.println("Current temperature: " + String(t) + " °C ");
}

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;
}

DynamicJsonDocument doc(768);

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.println("Callback");

  const String topicStr(topic);
  String payloadStr = convertPayloadToStr(payload, length);

  deserializeJson(doc, payloadStr);

  const String id((const char*)doc["id"]);

  Serial.println("Received, id=" + id);

  if (id == SETPOINT_NORMAL_ID) {
    const String targetTemp((const char*)doc["svalue1"]);
    sp = targetTemp.toFloat();
  }
  else if (id == CONTROL_ID) {
    const String targetMode((const char*)doc["svalue1"]);
    Serial.println("Set mode " + targetMode);
    if (targetMode == "0")
      heatingEnabled = false;
    else if (targetMode == "10" || targetMode == "20")
      heatingEnabled = true;
    else
      Serial.println("Unknown mode");
  }

  doc.clear();
}

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(OUT_TOPIC);
      client.setBufferSize(1024);
    } 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 = getTemp();
  ts = millis();
}

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

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

Put all things together

Master OpenTherm Shield Connection Domoticz OpenTherm Thermostat Shield


Domoticz UI in action

Domoticz Opentherm thermostat ui