Last active
July 21, 2023 21:31
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include "esphome.h" | |
class OpenthermBinarySensor : public BinarySensor { | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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(); | |
} | |
} | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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(); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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); | |
} | |
}; |
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
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 ;-)