Skip to content

Instantly share code, notes, and snippets.

@rsciriano
Last active July 21, 2023 21:31
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rsciriano/c78fc1e3f2d6afbb0295a2e5f4070174 to your computer and use it in GitHub Desktop.
Save rsciriano/c78fc1e3f2d6afbb0295a2e5f4070174 to your computer and use it in GitHub Desktop.
Code of a device, created with ESPHome, to integrate an OpenTherm boiler with Home Assistant
substitutions:
devicename: opentherm
upper_devicename: Opentherm
esphome:
name: $devicename
platform: ESP8266
board: d1_mini_lite
arduino_version: '2.7.2'
platformio_options:
lib_deps:
- ihormelnyk/OpenTherm Library @ 1.1.0
- ESP Async WebServer
includes:
- opentherm_component.h
- opentherm_climate.h
- opentherm_switch.h
- opentherm_binary.h
wifi:
ssid: !secret wifi_name
password: !secret wifi_password
# Enable logging
logger:
baud_rate: 74880
#level: DEBUG
api:
ota:
custom_component:
- lambda: |-
auto opentherm = new OpenthermComponent();
return {opentherm};
components:
- id: opentherm
sensor:
- platform: custom
lambda: |-
OpenthermComponent *openthermComp = (OpenthermComponent*) opentherm;
return {
openthermComp->boiler_temperature,
openthermComp->external_temperature_sensor,
openthermComp->return_temperature_sensor,
openthermComp->pressure_sensor,
openthermComp->modulation_sensor
};
sensors:
- name: "Boiler Temperature"
unit_of_measurement: °C
accuracy_decimals: 2
- name: "External Temperature"
unit_of_measurement: °C
accuracy_decimals: 0
- name: "Return Temperature"
unit_of_measurement: °C
accuracy_decimals: 2
- name: "Heating Water Pressure"
unit_of_measurement: hPa
accuracy_decimals: 2
- name: "Boiler Modulation"
unit_of_measurement: "%"
accuracy_decimals: 0
binary_sensor:
- platform: custom
lambda: |-
OpenthermComponent *openthermComp = (OpenthermComponent*) opentherm;
return {openthermComp->flame};
binary_sensors:
- name: "Flame"
#device_class: heat
switch:
- platform: custom
lambda: |-
OpenthermComponent *openthermComp = (OpenthermComponent*) opentherm;
return {openthermComp->thermostatSwitch};
switches:
name: "Termostato ambiente"
climate:
- platform: custom
lambda: |-
OpenthermComponent *openthermComp = (OpenthermComponent*) opentherm;
return {
openthermComp->hotWaterClimate,
openthermComp->heatingWaterClimate
};
climates:
- name: "Hot water"
- name: "Heating water"
#pragma once
#include "esphome.h"
class OpenthermBinarySensor : public BinarySensor {
};
#pragma once
#include "esphome.h"
class OpenthermClimate : public Climate {
private:
const char *TAG = "opentherm_climate";
public:
climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_supports_auto_mode(false);
traits.set_supports_cool_mode(false);
traits.set_supports_heat_mode(true);
traits.set_supports_two_point_target_temperature(false);
traits.set_supports_away(false);
traits.set_supports_action(true);
traits.set_visual_min_temperature(5);
traits.set_visual_max_temperature(80);
traits.set_visual_temperature_step(1);
return traits;
}
void control(const ClimateCall &call) override {
if (call.get_mode().has_value()) {
// User requested mode change
ClimateMode mode = *call.get_mode();
// Send mode to hardware
// ...
ESP_LOGD(TAG, "get_mode");
// Publish updated state
this->mode = mode;
this->publish_state();
}
if (call.get_target_temperature().has_value()) {
// User requested target temperature change
float temp = *call.get_target_temperature();
// Send target temp to climate
// ...
ESP_LOGD(TAG, "get_target_temperature");
this->target_temperature = temp;
this->publish_state();
}
}
};
#include "esphome.h"
#include "esphome/components/sensor/sensor.h"
#include "OpenTherm.h"
#include "opentherm_switch.h"
#include "opentherm_climate.h"
#include "opentherm_binary.h"
// Pins to OpenTherm Adapter
const int inPin = D5;
const int outPin = D6;
OpenTherm ot(inPin, outPin, false);
ICACHE_RAM_ATTR void handleInterrupt() {
ot.handleInterrupt();
}
class OpenthermComponent: public PollingComponent {
private:
const char *TAG = "opentherm_component";
public:
Switch *thermostatSwitch = new OpenthermSwitch();
Sensor *external_temperature_sensor = new Sensor();
Sensor *return_temperature_sensor = new Sensor();
Sensor *boiler_temperature = new Sensor();
Sensor *pressure_sensor = new Sensor();
Sensor *modulation_sensor = new Sensor();
Climate *hotWaterClimate = new OpenthermClimate();
Climate *heatingWaterClimate = new OpenthermClimate();
BinarySensor *flame = new OpenthermBinarySensor();
// Set 3 sec. to give time to read all sensors (and not appear in HA as not available)
OpenthermComponent(): PollingComponent(3000) {
}
void setup() override {
// This will be called once to set up the component
// think of it as the setup() call in Arduino
ESP_LOGD("opentherm_component", "Setup");
ot.begin(handleInterrupt);
thermostatSwitch->add_on_state_callback([=](bool state) -> void {
ESP_LOGD ("opentherm_component", "termostatSwitch_on_state_callback %d", state);
});
}
float getExternalTemperature() {
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0));
return ot.isValidResponse(response) ? ot.getFloat(response) : -1;
}
float getReturnTemperature() {
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tret, 0));
return ot.isValidResponse(response) ? ot.getFloat(response) : -1;
}
float getHotWaterTemperature() {
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tdhw, 0));
return ot.isValidResponse(response) ? ot.getFloat(response) : -1;
}
bool setHotWaterTemperature(float temperature) {
unsigned int data = ot.temperatureToData(temperature);
unsigned long request = ot.buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::TdhwSet, data);
unsigned long response = ot.sendRequest(request);
return ot.isValidResponse(response);
}
float getModulation() {
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0));
return ot.isValidResponse(response) ? ot.getFloat(response) : -1;
}
float getPressure() {
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0));
return ot.isValidResponse(response) ? ot.getFloat(response) : -1;
}
void update() override {
ESP_LOGD("opentherm_component", "update");
bool enableCentralHeating = heatingWaterClimate->mode == ClimateMode::CLIMATE_MODE_HEAT;
bool enableHotWater = hotWaterClimate->mode == ClimateMode::CLIMATE_MODE_HEAT;
bool enableCooling = false; // this boiler is for heating only
//Set/Get Boiler Status
auto response = ot.setBoilerStatus(enableCentralHeating, enableHotWater, enableCooling);
bool isFlameOn = ot.isFlameOn(response);
bool isCentralHeatingActive = ot.isCentralHeatingActive(response);
bool isHotWaterActive = ot.isHotWaterActive(response);
// Set temperature depending on room thermostat
if (thermostatSwitch->state) {
ot.setBoilerTemperature(heatingWaterClimate->target_temperature);
ESP_LOGD("opentherm_component", "setBoilerTemperature at %f °C (from heating water climate)", heatingWaterClimate->target_temperature);
}
else {
// If the room thermostat is off, set it to 10, so that the pump continues to operate
ot.setBoilerTemperature(10.0);
ESP_LOGD("opentherm_component", "setBoilerTemperature at %f °C (default low value)", 10.0);
}
// Set hot water temperature
setHotWaterTemperature(hotWaterClimate->target_temperature);
// Read sensor values
float boilerTemperature = ot.getBoilerTemperature();
float ext_temperature = getExternalTemperature();
float return_temperature = getReturnTemperature();
float hotWater_temperature = getHotWaterTemperature();
float pressure = getPressure();
float modulation = getModulation();
// Publish sensor values
flame->publish_state(isFlameOn);
external_temperature_sensor->publish_state(ext_temperature);
return_temperature_sensor->publish_state(return_temperature);
boiler_temperature->publish_state(boilerTemperature);
pressure_sensor->publish_state(pressure);
modulation_sensor->publish_state(modulation);
// Publish status of thermostat that controls hot water
hotWaterClimate->current_temperature = hotWater_temperature;
hotWaterClimate->action = isHotWaterActive ? ClimateAction::CLIMATE_ACTION_HEATING : ClimateAction::CLIMATE_ACTION_OFF;
hotWaterClimate->publish_state();
// Publish status of thermostat that controls heating
heatingWaterClimate->current_temperature = return_temperature;
heatingWaterClimate->action = isCentralHeatingActive && isFlameOn ? ClimateAction::CLIMATE_ACTION_HEATING : ClimateAction::CLIMATE_ACTION_OFF;
heatingWaterClimate->publish_state();
}
};
#pragma once
#include "esphome.h"
class OpenthermSwitch : public Switch {
public:
void write_state(bool state) override {
// This will be called every time the user requests a state change.
ESP_LOGD("opentherm_switch", "write_state");
// Acknowledge new state by publishing it
publish_state(state);
}
};
@rdehuyss
Copy link

Any chance you create a PR for this in ESPHome?

Nice work!

@rsciriano
Copy link
Author

Hi rdehuyss,

Yes, my intention is to contribute to ESPHome with the integration of OpenTherm.

I have ideas of how to do it but I need to take time (currently I'm busy with other things).

It's a challenge for me to express myself in English and program in Python, my native languages are Spanish and C# ;-)

Anyway, I am looking forward to making this contribution, I hope to have some free time soon

P.D: In my house I have the heating working with this code. So, we could say that it is suitable for production ;-)

@jperquin
Copy link

Happy to assist in any lingualities if needed. I am fairly fluent in English and Spanish (amongst others). When looking at your code, I see mostly comments in Spanish. Could take a pass at translating.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment