Help needed with arduino sketch Opentherm module by Ihor Melnyk

I am using home assistant for my thermostat function with multiple zones, using the generic thermostat integration.

My objective is to expand this with OpenTherm functionality rather than just on/off as my combination boiler can handle OpenTherm too. I bought the Opentherm module from Ihormelnyk.com, by Ihor Melnyk. He has written documentation on his website for a the integration in HA with MQTT.

However, the example on his website is by using an ESP2866 Wemos D1 mini. I personally would like to use a Arduino nano connected by USB instead or ethernet instead of wifi to connect the Opentherm module to home assistant (I want to limit the use of EMF radiation in the home). Also I don’t want to add the DS18b20 sensor to the arduino MCU, but use other temp sensors already available in my configuration.

I am a beginner, but usually can manage to make small change to code with manuals but it’s not my expertise. Therefore I am hoping someone can help me on the way a bit here.

The code for the sketch provided on the website (here) of Ihor Melnyk:

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

#include <OpenTherm.h>

//OpenTherm input and output wires connected to 4 and 5 pins on the OpenTherm Shield
const int inPin = 4;
const int outPin = 5;

//Data wire is connected to 14 pin on the OpenTherm Shield
#define ONE_WIRE_BUS 14

const char* ssid = "Please specify your WIFI SSID";
const char* password = "Please specify your WIFI password";
const char* mqtt_server = "Please specify MQTT server";
const int   mqtt_port = 00000;
const char* mqtt_user = "Please specify user";
const char* mqtt_password = "Please specify password";

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
OpenTherm ot(inPin, outPin);
WiFiClient espClient;
PubSubClient client(espClient);
char buf[10];

float sp = 23, //set point
pv = 0, //current temperature
pv_last = 0, //prior temperature
ierr = 0, //integral error
dt = 0, //time between measurements
op = 0; //PID controller output
unsigned long ts = 0, new_ts = 0; //timestamp


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

float getTemp() {
  return sensors.getTempCByIndex(0);
}

float pid(float sp, float pv, float pv_last, float& ierr, float dt) {
  float Kc = 10.0; // K / %Heater
  float tauI = 50.0; // sec
  float tauD = 1.0;  // sec
  // PID coefficients
  float KP = Kc;
  float KI = Kc / tauI;
  float KD = Kc*tauD; 
  // upper and lower bounds on heater level
  float ophi = 100;
  float oplo = 0;
  // 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 D = -KD * dpv; //derivative contribution
  float op = P + I + D;
  // 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) + " D=" + String(D));
  return op;
}

void setup_wifi() {
  delay(10);
  //Connect to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

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

  //Init DS18B20 Sensor
  sensors.begin();
  sensors.requestTemperatures();
  sensors.setWaitForConversion(false); //switch to async mode
  pv, pv_last = sensors.getTempCByIndex(0);
  ts = millis();

  //Init OpenTherm Controller
  ot.begin(handleInterrupt);

  //Init MQTT Client
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void publish_temperature() {
  Serial.println("t=" + String(pv));    
  String(pv).toCharArray(buf, 10);
  client.publish("pv", buf);  
}

void callback(char* topic, byte* payload, unsigned int length) {
  if(strcmp(topic, "sp") != 0) return;
  String str = String();    
  for (int i = 0; i < length; i++) {
    str += (char)payload[i];
  }
  Serial.println("sp=" + str);  
  sp = str.toFloat();
}

void reconnect() {  
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Client", mqtt_user, mqtt_password)) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      publish_temperature();
      // ... and resubscribe
      client.subscribe("sp");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }    
  }
}

void loop(void) { 
  new_ts = millis();
  if (new_ts - ts > 1000) {   
    //Set/Get Boiler Status
    bool enableCentralHeating = true;
    bool enableHotWater = true;
    bool enableCooling = false;
    unsigned long response = ot.setBoilerStatus(enableCentralHeating, enableHotWater, enableCooling);
    OpenThermResponseStatus responseStatus = ot.getLastResponseStatus();
    if (responseStatus != OpenThermResponseStatus::SUCCESS) {
      Serial.println("Error: Invalid boiler response " + String(response, HEX));
    }   

    pv = sensors.getTempCByIndex(0);
    dt = (new_ts - ts) / 1000.0;
    ts = new_ts;
    if (responseStatus == OpenThermResponseStatus::SUCCESS) {
      op = pid(sp, pv, pv_last, ierr, dt);
      //Set Boiler Temperature
      ot.setBoilerTemperature(op);
    }
    pv_last = pv;
    
    sensors.requestTemperatures(); //async temperature request
    
    publish_temperature();
  }
  
  //MQTT Loop
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

What comes to my mind is that I have to add a mqtt possibilty over USB, how can I do that?
And I have to remove the wifi parts and the library calling of <ESP8266WiFi.h>.

Could someone help me on the way on how to alter this code for my situation?

Thanks

Johan

I did some work on the above and decided to try a configuration with a Enc28j60 module ethernet module and a arduino nano. I read message on this forum and elsewhere that ethernet modules in combination with arduino are really unreliable, especially the enc28j60. But I’ll first try to get it working, then maybe switch to a better option like a ESP32 with ethernet.

I edited the sketch provided by Ihor Melnyk, to I think is right. But I will need to test it further.

Found another tutorial about this module and home assistant which is also helpful.

If I get it working right, will post my sketch and config so maybe others can use it too.

I worked further on this project and got it working with a W5500 ethernet module and a Arduino nano.

Here is the Arduino sketch I used:

#include <PubSubClient.h>
#include <OpenTherm.h>
#include <Ethernet2.h>


//OpenTherm input and output wires connected to 4 and 5 pins on the OpenTherm Shield
const int inPin = 3;
const int outPin = 2;

byte mac[] = { 0x54, 0x34, 0x41, 0x30, 0x30, 0x31 };                                      
IPAddress ip(192, 168, 1, 179);                        
EthernetServer server(80);

const char* mqtt_server = "192.168.1.120";
const int   mqtt_port = 1883;
const char* mqtt_user = "xxx";
const char* mqtt_password = "xxx";

OpenTherm ot(inPin, outPin);
EthernetClient ethClient;
PubSubClient client(ethClient);
char buf[10];

float sp = 18, //set point
pv = 25, //current temperature
pv_last = 24, //prior temperature
ierr = 0, //integral error
dt = 1000, //time between measurements
op = 0; //PID controller output
unsigned long ts = 0, new_ts = 0; //timestamp


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

float pid(float sp, float pv, float pv_last, float& ierr, float dt) {
  float Kc = 10.0; // K / %Heater
  float tauI = 50.0; // sec
  float tauD = 1.0;  // sec
  // PID coefficients
  float KP = Kc;
  float KI = Kc / tauI;
  float KD = Kc*tauD; 
  // upper and lower bounds on heater level
  float ophi = 100;
  float oplo = 0;
  // 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 D = -KD * dpv; //derivative contribution
  float op = P + I + D;
  // 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) + " D=" + String(D));
  return op;
}



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

  ts = millis();

  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();

  Serial.print("IP Address: ");
  Serial.println(Ethernet.localIP());

  //Init OpenTherm Controller
  ot.begin(handleInterrupt);

  //Init MQTT Client
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void publish_temperature() {
  Serial.println("t=" + String(pv));    
  String(pv).toCharArray(buf, 10);
  client.publish("pv", buf);  
}

void callback(char* topic, byte* payload, unsigned int length) {
  if(strcmp(topic, "sp") != 0) return;
  String str = String();    
  for (int i = 0; i < length; i++) {
    str += (char)payload[i];
  }
  Serial.println("sp=" + str);  
  sp = str.toFloat();
}

void reconnect() {  
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect  
      if (client.connect("ethClient", mqtt_user, mqtt_password)) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      publish_temperature();
      // ... and resubscribe
      client.subscribe("sp");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }    
  }
}

void loop(void) { 
  new_ts = millis();
  if (new_ts - ts > 1000) {   
    //Set/Get Boiler Status
    bool enableCentralHeating = true;
    bool enableHotWater = true;
    bool enableCooling = false;
    unsigned long response = ot.setBoilerStatus(enableCentralHeating, enableHotWater, enableCooling);
    OpenThermResponseStatus responseStatus = ot.getLastResponseStatus();
    if (responseStatus != OpenThermResponseStatus::SUCCESS) {
      Serial.println("Error: Invalid boiler response " + String(response, HEX));
    }   

    pv = 25;
    dt = (new_ts - ts) / 1000.0;
    ts = new_ts;
    if (responseStatus == OpenThermResponseStatus::SUCCESS) {
      op = pid(sp, pv, pv_last, ierr, dt);
      //Set Boiler Temperature
      ot.setBoilerTemperature(op);
    }
    pv_last = pv;
        
    publish_temperature();
  }
  
  //MQTT Loop
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

Which gave me the output in Serial Monitor:

Above sketch is the sketch provided on this tutorial with edits. As I am using a W5500 ethernet module, I removed the parts for ESP8266 and wifi and added the Ethernet2 library.

I also don’t use a DS18b20 on the arduino mcu, but use multiple temp sensors within my home as a base for the thermostat function. Therefore also the the parts in relationship to the DS18b20 like the Dallas library were removed and I’ve set the current initital temperature to a fixed value at first.

Probably there are other ways to do this, rather than setting a fixed value, but I am not a coder.

I still need to use it in practice so I haven’t tested it thoroughly. I have multiple zones for heating so I am planning to send the current roomtemperature and desired temperature to the boiler for the zone with largest current difference between these two variables. But I have to make automations for that I didn’t take the time yet.

Also some notes:

  • I was only able to upload the sketch to my Arduino nano when I powered off the W5500 module, by disconnecting the 5v jumper wire.
  • For the opentherm module it’s needed to use pins on the arduino usable for interrupts, see here. These are pins 2 and 3 on the Nano.
  • It’s a good idea to first connect only the Opentherm module to the arduino and run the Full code example which is provided here. When you get a desired output on the Serial Monitor of Arduino IDE, then you know that part works and you can go further with adding the Ethernet module and make MQTT connection to home assistant.
  • As W5500 Ethernet module, I used the Robotdyn w5500 Ethernet module. More on how to wire the pins to an arduino: GitHub - johanf85/robotdyn-w5500-arduino-nano

Ethernet modules with Arduino are extremely reliable (except ENC28J60) if the module is built properly.

Honestly I would not consider ESP32 with Ethernet a “better” option, it is just another option. Could be better depending on your application, could be the same.

One thing I noted about your code is that it won’t run reliably long term. Because it uses the String class, it will eventually fragment memory to the point the processor runs out then locks. This combined with the large number of literal strings used in serial.print which eat memory.

Don’t use String. Use character arrays (char) referred to as cstrings. They use fixed memory allocations.

Surround all literal strings in serial.print with the F macro to store them in progmem. Like: serial.print(F(“this string can be up to about 30K big and not use any RAM to print because it is printed directly from flash memory, not copied to RAM then printed”));

Hi @AaronCake , thanks for your reply and recommendations. Good to hear that a W5500 in combination with arduino is reliable (if the module is right). I will look at the use of String.