├── .gitignore ├── README.md ├── components └── opentherm │ ├── __init__.py │ ├── opentherm.cpp │ ├── opentherm.h │ ├── opentherm_gw_climate.cpp │ └── opentherm_gw_climate.h ├── doc ├── Opentherm Protocol v2-2.pdf └── messages.csv ├── opentherm.yaml ├── opentherm_esp8266.yaml └── simple_thermostat.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | /.vscode/ 6 | **/.pioenvs/ 7 | **/.piolibdeps/ 8 | **/lib/ 9 | **/src/ 10 | **/platformio.ini 11 | /secrets.yaml 12 | /.vs 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTherm 2 | Port of OpenTherm protocol to ESPHome.io firmware. 3 | 4 | Hardware: 5 | https://github.com/jpraus/arduino-opentherm 6 | http://ihormelnyk.com/opentherm_adapter 7 | 8 | ## Support my work 9 | Thank you for thinking about supporting my work. 10 | 11 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/wichers) -------------------------------------------------------------------------------- /components/opentherm/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.config_validation as cv 2 | import esphome.codegen as cg 3 | from esphome.const import * 4 | from esphome.components import climate 5 | from esphome.components import sensor 6 | from esphome.components import binary_sensor 7 | from esphome import pins 8 | 9 | openthermgw_ns = cg.esphome_ns.namespace("opentherm") 10 | OpenThermGWComponent = openthermgw_ns.class_("OpenThermGWClimate", cg.Component) 11 | 12 | AUTO_LOAD = ["sensor", "climate", "binary_sensor"] 13 | CONF_HUB_ID = "opentherm" 14 | 15 | UNIT_HOURS = "h" 16 | UNIT_BAR = "bar" 17 | 18 | CONF_IS_CH2_ACTIVE = "is_ch2_active" 19 | CONF_IS_CH_ACTIVE = "is_ch_active" 20 | CONF_IS_COOLING_ACTIVE = "is_cooling_active" 21 | CONF_IS_DHW_ACTIVE = "is_dhw_active" 22 | CONF_IS_DIAGNOSTIC_EVENT = "is_diagnostic_event" 23 | CONF_IS_FAULT_INDICATION = "is_fault_indication" 24 | CONF_IS_FLAME_ON = "is_flame_on" 25 | CONF_BOILER_WATER_TEMP = "boiler_water_temp" 26 | CONF_BURNER_OPERATION_HOURS = "burner_operation_hours" 27 | CONF_BURNER_STARTS = "burner_starts" 28 | CONF_CH_PUMP_OPERATION_HOURS = "ch_pump_operation_hours" 29 | CONF_CH_PUMP_STARTS = "ch_pump_starts" 30 | CONF_CH_WATER_PRESSURE = "ch_water_pressure" 31 | CONF_DHW2_TEMPERATURE = "dhw2_temperature" 32 | CONF_DHW_BURNER_OPERATION_HOURS = "dhw_burner_operation_hours" 33 | CONF_DHW_BURNER_STARTS = "dhw_burner_starts" 34 | CONF_DHW_FLOW_RATE = "dhw_flow_rate" 35 | CONF_DHW_PUMP_VALVE_OPERATION_HOURS = "dhw_pump_valve_operation_hours" 36 | CONF_DHW_PUMP_VALVE_STARTS = "dhw_pump_valve_starts" 37 | CONF_DHW_TEMPERATURE = "dhw_temperature" 38 | CONF_EXHAUST_TEMPERATURE = "exhaust_temperature" 39 | CONF_FLOW_TEMPERATURE_CH2 = "flow_temperature_ch2" 40 | CONF_OUTSIDE_AIR_TEMPERATURE = "outside_air_temperature" 41 | CONF_RELATIVE_MODULATION_LEVEL = "relative_modulation_level" 42 | CONF_RETURN_WATER_TEMPERATURE = "return_water_temperature" 43 | CONF_SOLAR_COLLECTOR_TEMPERATURE = "solar_collector_temperature" 44 | CONF_SOLAR_STORAGE_TEMPERATURE = "solar_storage_temperature" 45 | CONF_THERMOSTAT_IN_PIN = "thermostat_in_pin" 46 | CONF_THERMOSTAT_OUT_PIN = "thermostat_out_pin" 47 | CONF_BOILER_IN_PIN = "boiler_in_pin" 48 | CONF_BOILER_OUT_PIN = "boiler_out_pin" 49 | 50 | helper_opentherm_list = [ 51 | CONF_BOILER_WATER_TEMP, 52 | CONF_BURNER_OPERATION_HOURS, 53 | CONF_BURNER_STARTS, 54 | CONF_CH_PUMP_OPERATION_HOURS, 55 | CONF_CH_PUMP_STARTS, 56 | CONF_CH_WATER_PRESSURE, 57 | CONF_DHW2_TEMPERATURE, 58 | CONF_DHW_BURNER_OPERATION_HOURS, 59 | CONF_DHW_BURNER_STARTS, 60 | CONF_DHW_FLOW_RATE, 61 | CONF_DHW_PUMP_VALVE_OPERATION_HOURS, 62 | CONF_DHW_PUMP_VALVE_STARTS, 63 | CONF_DHW_TEMPERATURE, 64 | CONF_EXHAUST_TEMPERATURE, 65 | CONF_FLOW_TEMPERATURE_CH2, 66 | CONF_IS_CH2_ACTIVE, 67 | CONF_IS_CH_ACTIVE, 68 | CONF_IS_COOLING_ACTIVE, 69 | CONF_IS_DHW_ACTIVE, 70 | CONF_IS_DIAGNOSTIC_EVENT, 71 | CONF_IS_FAULT_INDICATION, 72 | CONF_IS_FLAME_ON, 73 | CONF_OUTSIDE_AIR_TEMPERATURE, 74 | CONF_RELATIVE_MODULATION_LEVEL, 75 | CONF_RETURN_WATER_TEMPERATURE, 76 | CONF_SOLAR_COLLECTOR_TEMPERATURE, 77 | CONF_SOLAR_STORAGE_TEMPERATURE, 78 | ] 79 | 80 | opentherm_sensors_schemas = cv.Schema( 81 | { 82 | cv.Optional(CONF_BOILER_WATER_TEMP): sensor.sensor_schema( 83 | device_class=DEVICE_CLASS_TEMPERATURE, 84 | unit_of_measurement=UNIT_CELSIUS, 85 | accuracy_decimals=1, 86 | state_class=STATE_CLASS_MEASUREMENT, 87 | ).extend(), 88 | cv.Optional(CONF_BURNER_OPERATION_HOURS): sensor.sensor_schema( 89 | unit_of_measurement=UNIT_HOURS, 90 | accuracy_decimals=0, 91 | state_class=STATE_CLASS_TOTAL_INCREASING, 92 | ).extend(), 93 | cv.Optional(CONF_BURNER_STARTS): sensor.sensor_schema( 94 | unit_of_measurement=UNIT_METER, 95 | accuracy_decimals=1, 96 | state_class=STATE_CLASS_MEASUREMENT, 97 | ).extend(), 98 | cv.Optional(CONF_CH_PUMP_OPERATION_HOURS): sensor.sensor_schema( 99 | unit_of_measurement=UNIT_HOURS, 100 | accuracy_decimals=0, 101 | state_class=STATE_CLASS_TOTAL_INCREASING, 102 | ).extend(), 103 | cv.Optional(CONF_CH_PUMP_STARTS): sensor.sensor_schema( 104 | unit_of_measurement=UNIT_METER, 105 | accuracy_decimals=1, 106 | state_class=STATE_CLASS_MEASUREMENT, 107 | ).extend(), 108 | cv.Optional(CONF_CH_WATER_PRESSURE): sensor.sensor_schema( 109 | device_class=DEVICE_CLASS_PRESSURE, 110 | unit_of_measurement=UNIT_BAR, 111 | accuracy_decimals=1, 112 | state_class=STATE_CLASS_MEASUREMENT, 113 | ).extend(), 114 | cv.Optional(CONF_DHW2_TEMPERATURE): sensor.sensor_schema( 115 | device_class=DEVICE_CLASS_TEMPERATURE, 116 | unit_of_measurement=UNIT_CELSIUS, 117 | accuracy_decimals=1, 118 | state_class=STATE_CLASS_MEASUREMENT, 119 | ).extend(), 120 | cv.Optional(CONF_DHW_BURNER_OPERATION_HOURS): sensor.sensor_schema( 121 | unit_of_measurement=UNIT_HOURS, 122 | accuracy_decimals=0, 123 | state_class=STATE_CLASS_TOTAL_INCREASING, 124 | ).extend(), 125 | cv.Optional(CONF_DHW_BURNER_STARTS): sensor.sensor_schema( 126 | unit_of_measurement=UNIT_METER, 127 | accuracy_decimals=1, 128 | state_class=STATE_CLASS_MEASUREMENT, 129 | ).extend(), 130 | cv.Optional(CONF_DHW_FLOW_RATE): sensor.sensor_schema( 131 | unit_of_measurement=UNIT_METER, 132 | accuracy_decimals=1, 133 | state_class=STATE_CLASS_MEASUREMENT, 134 | ).extend(), 135 | cv.Optional(CONF_DHW_PUMP_VALVE_OPERATION_HOURS): sensor.sensor_schema( 136 | unit_of_measurement=UNIT_HOURS, 137 | accuracy_decimals=0, 138 | state_class=STATE_CLASS_TOTAL_INCREASING, 139 | ).extend(), 140 | cv.Optional(CONF_DHW_PUMP_VALVE_STARTS): sensor.sensor_schema( 141 | unit_of_measurement=UNIT_METER, 142 | accuracy_decimals=1, 143 | state_class=STATE_CLASS_MEASUREMENT, 144 | ).extend(), 145 | cv.Optional(CONF_DHW_TEMPERATURE): sensor.sensor_schema( 146 | device_class=DEVICE_CLASS_TEMPERATURE, 147 | unit_of_measurement=UNIT_CELSIUS, 148 | accuracy_decimals=1, 149 | state_class=STATE_CLASS_MEASUREMENT, 150 | ).extend(), 151 | cv.Optional(CONF_EXHAUST_TEMPERATURE): sensor.sensor_schema( 152 | device_class=DEVICE_CLASS_TEMPERATURE, 153 | unit_of_measurement=UNIT_CELSIUS, 154 | accuracy_decimals=1, 155 | state_class=STATE_CLASS_MEASUREMENT, 156 | ).extend(), 157 | cv.Optional(CONF_FLOW_TEMPERATURE_CH2): sensor.sensor_schema( 158 | device_class=DEVICE_CLASS_TEMPERATURE, 159 | unit_of_measurement=UNIT_CELSIUS, 160 | accuracy_decimals=1, 161 | state_class=STATE_CLASS_MEASUREMENT, 162 | ).extend(), 163 | cv.Optional(CONF_OUTSIDE_AIR_TEMPERATURE): sensor.sensor_schema( 164 | device_class=DEVICE_CLASS_TEMPERATURE, 165 | unit_of_measurement=UNIT_CELSIUS, 166 | accuracy_decimals=1, 167 | state_class=STATE_CLASS_MEASUREMENT, 168 | ).extend(), 169 | cv.Optional(CONF_RELATIVE_MODULATION_LEVEL): sensor.sensor_schema( 170 | icon=ICON_PERCENT, 171 | unit_of_measurement=UNIT_PERCENT, 172 | accuracy_decimals=1, 173 | state_class=STATE_CLASS_MEASUREMENT, 174 | ).extend(), 175 | cv.Optional(CONF_RETURN_WATER_TEMPERATURE): sensor.sensor_schema( 176 | device_class=DEVICE_CLASS_TEMPERATURE, 177 | unit_of_measurement=UNIT_CELSIUS, 178 | accuracy_decimals=1, 179 | state_class=STATE_CLASS_MEASUREMENT, 180 | ).extend(), 181 | cv.Optional(CONF_SOLAR_COLLECTOR_TEMPERATURE): sensor.sensor_schema( 182 | device_class=DEVICE_CLASS_TEMPERATURE, 183 | unit_of_measurement=UNIT_CELSIUS, 184 | accuracy_decimals=1, 185 | state_class=STATE_CLASS_MEASUREMENT, 186 | ).extend(), 187 | cv.Optional(CONF_SOLAR_STORAGE_TEMPERATURE): sensor.sensor_schema( 188 | device_class=DEVICE_CLASS_TEMPERATURE, 189 | unit_of_measurement=UNIT_CELSIUS, 190 | accuracy_decimals=1, 191 | state_class=STATE_CLASS_MEASUREMENT, 192 | ).extend(), 193 | cv.Optional(CONF_IS_CH2_ACTIVE): binary_sensor.binary_sensor_schema( 194 | device_class=DEVICE_CLASS_EMPTY 195 | ).extend(), 196 | cv.Optional(CONF_IS_CH_ACTIVE): binary_sensor.binary_sensor_schema( 197 | device_class=DEVICE_CLASS_EMPTY 198 | ).extend(), 199 | cv.Optional(CONF_IS_COOLING_ACTIVE): binary_sensor.binary_sensor_schema( 200 | device_class=DEVICE_CLASS_EMPTY 201 | ).extend(), 202 | cv.Optional(CONF_IS_DHW_ACTIVE): binary_sensor.binary_sensor_schema( 203 | device_class=DEVICE_CLASS_EMPTY 204 | ).extend(), 205 | cv.Optional(CONF_IS_DIAGNOSTIC_EVENT): binary_sensor.binary_sensor_schema( 206 | device_class=DEVICE_CLASS_EMPTY 207 | ).extend(), 208 | cv.Optional(CONF_IS_FAULT_INDICATION): binary_sensor.binary_sensor_schema( 209 | device_class=DEVICE_CLASS_EMPTY 210 | ).extend(), 211 | cv.Optional(CONF_IS_FLAME_ON): binary_sensor.binary_sensor_schema( 212 | device_class=DEVICE_CLASS_EMPTY 213 | ).extend(), 214 | } 215 | ) 216 | 217 | CONFIG_SCHEMA = cv.All( 218 | cv.Schema( 219 | { 220 | cv.GenerateID(CONF_ID): cv.declare_id(OpenThermGWComponent), 221 | cv.Required(CONF_THERMOSTAT_IN_PIN): pins.internal_gpio_input_pin_schema, 222 | cv.Required(CONF_THERMOSTAT_OUT_PIN): pins.internal_gpio_input_pin_schema, 223 | cv.Required(CONF_BOILER_IN_PIN): pins.internal_gpio_input_pin_schema, 224 | cv.Required(CONF_BOILER_OUT_PIN): pins.internal_gpio_input_pin_schema, 225 | } 226 | ) 227 | .extend(opentherm_sensors_schemas) 228 | .extend(cv.COMPONENT_SCHEMA) 229 | ) 230 | 231 | 232 | def to_code(config): 233 | var = cg.new_Pvariable(config[CONF_ID]) 234 | yield cg.register_component(var, config) 235 | 236 | thermostat_in_pin = yield cg.gpio_pin_expression(config[CONF_THERMOSTAT_IN_PIN]) 237 | cg.add(var.set_thermostat_in_pin(thermostat_in_pin)) 238 | thermostat_out_pin = yield cg.gpio_pin_expression(config[CONF_THERMOSTAT_OUT_PIN]) 239 | cg.add(var.set_thermostat_out_pin(thermostat_out_pin)) 240 | boiler_in_pin = yield cg.gpio_pin_expression(config[CONF_BOILER_IN_PIN]) 241 | cg.add(var.set_boiler_in_pin(boiler_in_pin)) 242 | boiler_out_pin = yield cg.gpio_pin_expression(config[CONF_BOILER_OUT_PIN]) 243 | cg.add(var.set_boiler_out_pin(boiler_out_pin)) 244 | for k in helper_opentherm_list: 245 | if k in config: 246 | sens = None 247 | if "is_" in k: 248 | sens = yield binary_sensor.new_binary_sensor(config[k]) 249 | else: 250 | sens = yield sensor.new_sensor(config[k]) 251 | func = getattr(var, "set_" + k) 252 | cg.add(func(sens)) 253 | 254 | cg.add(cg.App.register_climate(var)) 255 | -------------------------------------------------------------------------------- /components/opentherm/opentherm.cpp: -------------------------------------------------------------------------------- 1 | /* OpenTherm.cpp - OpenTherm Communication Library For Arduino, ESP8266 2 | Copyright 2018, Ihor Melnyk */ 3 | 4 | #include "opentherm.h" 5 | #include 6 | 7 | namespace esphome { 8 | namespace opentherm { 9 | 10 | OpenThermChannel::OpenThermChannel(bool slave): 11 | isSlave(slave), 12 | store_(slave) 13 | { 14 | } 15 | 16 | OpenThermChannel::~OpenThermChannel() { 17 | this->pin_in_->detach_interrupt(); 18 | } 19 | 20 | void OpenThermChannel::setup(std::function callback) 21 | { 22 | this->pin_in_->setup(); 23 | this->store_.pin_in = this->pin_in_->to_isr(); 24 | 25 | this->pin_out_->setup(); 26 | 27 | this->pin_in_->attach_interrupt(OpenThermStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); 28 | 29 | activateBoiler(); 30 | this->store_.status = OpenThermStatus::READY; 31 | this->process_response_callback = callback; 32 | } 33 | 34 | void OpenThermChannel::loop() 35 | { 36 | OpenThermStatus st; 37 | uint32_t ts; 38 | { 39 | InterruptLock lock; 40 | st = this->store_.status; 41 | ts = this->store_.responseTimestamp; 42 | } 43 | 44 | if (st == OpenThermStatus::READY) return; 45 | uint32_t newTs = micros(); 46 | if (st != OpenThermStatus::NOT_INITIALIZED && (newTs - ts) > 1000000) { 47 | this->store_.status = OpenThermStatus::READY; 48 | responseStatus = OpenThermResponseStatus::TIMEOUT; 49 | if (process_response_callback) { 50 | process_response_callback(this->store_.response, responseStatus); 51 | } 52 | } 53 | else if (st == OpenThermStatus::RESPONSE_INVALID) { 54 | this->store_.status = OpenThermStatus::DELAY; 55 | responseStatus = OpenThermResponseStatus::INVSTART; 56 | if (process_response_callback) { 57 | process_response_callback(this->store_.response, responseStatus); 58 | } 59 | } 60 | else if (st == OpenThermStatus::RESPONSE_READY) { 61 | this->store_.status = OpenThermStatus::DELAY; 62 | if (parity(this->store_.response)) 63 | responseStatus = OpenThermResponseStatus::INVPARITY; 64 | else if (isSlave) 65 | responseStatus = isValidResponse(this->store_.response) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVMSGTYPE; 66 | else 67 | responseStatus = isValidRequest(this->store_.response) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVMSGTYPE; 68 | if (process_response_callback) { 69 | process_response_callback(this->store_.response, responseStatus); 70 | } 71 | } 72 | else if (st == OpenThermStatus::DELAY) { 73 | if ((newTs - ts) > 100000) { 74 | this->store_.status = OpenThermStatus::READY; 75 | } 76 | } 77 | } 78 | 79 | bool OpenThermChannel::isReady() 80 | { 81 | return this->store_.status == OpenThermStatus::READY; 82 | } 83 | 84 | void OpenThermChannel::setActiveState() { 85 | this->pin_out_->digital_write(false); 86 | } 87 | 88 | void OpenThermChannel::setIdleState() { 89 | this->pin_out_->digital_write(true); 90 | } 91 | 92 | void OpenThermChannel::activateBoiler() { 93 | setIdleState(); 94 | delay(1000); 95 | } 96 | 97 | void OpenThermChannel::sendBit(bool high) { 98 | if (high) setActiveState(); else setIdleState(); 99 | delayMicroseconds(500); 100 | if (high) setIdleState(); else setActiveState(); 101 | delayMicroseconds(500); 102 | } 103 | 104 | bool OpenThermChannel::sendRequestAync(uint32_t request) 105 | { 106 | bool ready; 107 | { 108 | InterruptLock lock; 109 | ready = isReady(); 110 | } 111 | 112 | if (!ready) 113 | return false; 114 | 115 | this->store_.status = OpenThermStatus::REQUEST_SENDING; 116 | this->store_.response = 0; 117 | responseStatus = OpenThermResponseStatus::NONE; 118 | 119 | sendBit(true); //start bit 120 | for (int i = 31; i >= 0; i--) { 121 | sendBit((request & (1 << i)) != 0); 122 | } 123 | sendBit(true); //stop bit 124 | setIdleState(); 125 | 126 | this->store_.status = OpenThermStatus::RESPONSE_WAITING; 127 | this->store_.responseTimestamp = micros(); 128 | return true; 129 | } 130 | 131 | uint32_t OpenThermChannel::sendRequest(uint32_t request) 132 | { 133 | if (!sendRequestAync(request)) return 0; 134 | while (!isReady()) { 135 | loop(); 136 | yield(); 137 | } 138 | return this->store_.response; 139 | } 140 | 141 | bool OpenThermChannel::sendResponse(uint32_t request) 142 | { 143 | this->store_.status = OpenThermStatus::REQUEST_SENDING; 144 | this->store_.response = 0; 145 | responseStatus = OpenThermResponseStatus::NONE; 146 | 147 | sendBit(true); //start bit 148 | for (int i = 31; i >= 0; i--) { 149 | sendBit((request & (1 << i)) != 0); 150 | } 151 | sendBit(true); //stop bit 152 | setIdleState(); 153 | this->store_.status = OpenThermStatus::READY; 154 | return true; 155 | } 156 | 157 | OpenThermResponseStatus OpenThermChannel::getLastResponseStatus() 158 | { 159 | return responseStatus; 160 | } 161 | 162 | void IRAM_ATTR OpenThermStore::gpio_intr(OpenThermStore *arg) 163 | { 164 | if (arg->status == OpenThermStatus::READY) 165 | { 166 | if (!arg->isSlave && arg->pin_in.digital_read()) { 167 | arg->status = OpenThermStatus::RESPONSE_WAITING; 168 | } 169 | else { 170 | return; 171 | } 172 | } 173 | 174 | uint32_t newTs = micros(); 175 | if (arg->status == OpenThermStatus::RESPONSE_WAITING) { 176 | if (arg->pin_in.digital_read()) { 177 | arg->status = OpenThermStatus::RESPONSE_START_BIT; 178 | arg->responseTimestamp = newTs; 179 | } 180 | else { 181 | arg->status = OpenThermStatus::RESPONSE_INVALID; 182 | arg->responseTimestamp = newTs; 183 | } 184 | } 185 | else if (arg->status == OpenThermStatus::RESPONSE_START_BIT) { 186 | if ((newTs - arg->responseTimestamp < 750) && !arg->pin_in.digital_read()) { 187 | arg->status = OpenThermStatus::RESPONSE_RECEIVING; 188 | arg->responseTimestamp = newTs; 189 | arg->responseBitIndex = 0; 190 | } 191 | else { 192 | arg->status = OpenThermStatus::RESPONSE_INVALID; 193 | arg->responseTimestamp = newTs; 194 | } 195 | } 196 | else if (arg->status == OpenThermStatus::RESPONSE_RECEIVING) { 197 | if ((newTs - arg->responseTimestamp) > 750) { 198 | if (arg->responseBitIndex < 32) { 199 | arg->response = (arg->response << 1) | !arg->pin_in.digital_read(); 200 | arg->responseTimestamp = newTs; 201 | arg->responseBitIndex++; 202 | } 203 | else { //stop bit 204 | arg->status = OpenThermStatus::RESPONSE_READY; 205 | arg->responseTimestamp = newTs; 206 | } 207 | } 208 | } 209 | } 210 | 211 | bool parity(uint32_t frame) //odd parity 212 | { 213 | uint8_t p = 0; 214 | while (frame > 0) 215 | { 216 | if (frame & 1) p++; 217 | frame = frame >> 1; 218 | } 219 | return (p & 1); 220 | } 221 | 222 | OpenThermMessageType getMessageType(uint32_t message) 223 | { 224 | OpenThermMessageType msg_type = static_cast((message >> 28) & 7); 225 | return msg_type; 226 | } 227 | 228 | OpenThermMessageID getDataID(uint32_t frame) 229 | { 230 | return (OpenThermMessageID)((frame >> 16) & 0xFF); 231 | } 232 | 233 | uint32_t buildRequest(OpenThermMessageType type, OpenThermMessageID id, uint16_t data) 234 | { 235 | uint32_t request = data; 236 | if (type == OpenThermMessageType::WRITE_DATA) { 237 | request |= 1ul << 28; 238 | } 239 | request |= ((uint32_t)id) << 16; 240 | if (parity(request)) request |= (1ul << 31); 241 | return request; 242 | } 243 | 244 | uint32_t buildResponse(OpenThermMessageType type, OpenThermMessageID id, uint16_t data) 245 | { 246 | uint32_t response = data; 247 | response |= type << 28; 248 | response |= ((uint32_t)id) << 16; 249 | if (parity(response)) response |= (1ul << 31); 250 | return response; 251 | } 252 | 253 | uint32_t modifyMsgData(uint32_t msg, uint16_t data) 254 | { 255 | msg = (msg & 0x7fff0000) | data; 256 | if (parity(msg)) msg |= (1ul << 31); 257 | return msg; 258 | } 259 | 260 | bool isValidResponse(uint32_t response) 261 | { 262 | uint8_t msgType = (response << 1) >> 29; 263 | return msgType == READ_ACK || msgType == WRITE_ACK || msgType == DATA_INVALID || msgType == UNKNOWN_DATA_ID; 264 | } 265 | 266 | bool isValidRequest(uint32_t request) 267 | { 268 | uint8_t msgType = (request << 1) >> 29; 269 | return msgType == READ_DATA || msgType == WRITE_DATA || msgType == INVALID_DATA; 270 | } 271 | 272 | const char *statusToString(OpenThermResponseStatus status) 273 | { 274 | switch (status) { 275 | case NONE: return "NONE"; 276 | case SUCCESS: return "SUCCESS"; 277 | case TIMEOUT: return "TIMEOUT"; 278 | case INVSTART: return "INVSTART"; 279 | case INVPARITY: return "INVPARITY"; 280 | case INVMSGTYPE: return "INVMSGTYPE"; 281 | } 282 | return "UNKNOWN"; 283 | } 284 | 285 | const char *messageTypeToString(OpenThermMessageType message_type) 286 | { 287 | switch (message_type) { 288 | case READ_DATA: return "READ_DATA"; 289 | case WRITE_DATA: return "WRITE_DATA"; 290 | case INVALID_DATA: return "INVALID_DATA"; 291 | case RESERVED: return "RESERVED"; 292 | case READ_ACK: return "READ_ACK"; 293 | case WRITE_ACK: return "WRITE_ACK"; 294 | case DATA_INVALID: return "DATA_INVALID"; 295 | case UNKNOWN_DATA_ID: return "UNKNOWN_DATA_ID"; 296 | default: return "UNKNOWN"; 297 | } 298 | } 299 | 300 | uint8_t getUBUInt8(const uint32_t response) { 301 | return (response >> 8) & 0xff; 302 | } 303 | 304 | uint8_t getLBUInt8(const uint32_t response) { 305 | return response & 0xff; 306 | } 307 | 308 | int8_t getUBInt8(const uint32_t response) { 309 | return (int8_t) ((response >> 8) & 0xff); 310 | } 311 | 312 | int8_t getLBInt8(const uint32_t response) { 313 | return (int8_t) (response & 0xff); 314 | } 315 | 316 | uint16_t getUInt16(const uint32_t response) { 317 | return response & 0xffff; 318 | } 319 | 320 | int16_t getInt16(const uint32_t response) { 321 | return (int16_t) (response & 0xffff); 322 | } 323 | 324 | float getFloat(const uint32_t response) { 325 | const uint16_t u88 = getUInt16(response); 326 | const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f; 327 | return f; 328 | } 329 | 330 | uint16_t temperatureToData(float temperature) { 331 | if (temperature < 0) temperature = 0; 332 | if (temperature > 100) temperature = 100; 333 | uint16_t data = (uint16_t)(temperature * 256); 334 | return data; 335 | } 336 | 337 | } // namespace opentherm 338 | } // namespace esphome 339 | -------------------------------------------------------------------------------- /components/opentherm/opentherm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | /* 3 | OpenTherm.h - OpenTherm Library for the ESP8266/Arduino platform 4 | https://github.com/ihormelnyk/OpenTherm 5 | http://ihormelnyk.com/pages/OpenTherm 6 | Licensed under MIT license 7 | Copyright 2018, Ihor Melnyk 8 | 9 | Frame Structure: 10 | P MGS-TYPE SPARE DATA-ID DATA-VALUE 11 | 0 000 0000 00000000 00000000 00000000 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | namespace esphome { 19 | namespace opentherm { 20 | 21 | enum OpenThermResponseStatus { 22 | NONE, 23 | SUCCESS, 24 | TIMEOUT, 25 | INVSTART, 26 | INVPARITY, 27 | INVMSGTYPE, 28 | }; 29 | 30 | 31 | enum OpenThermMessageType { 32 | /* Master to Slave */ 33 | READ_DATA = 0, 34 | READ = READ_DATA, // for backwared compatibility 35 | WRITE_DATA = 1, 36 | WRITE = WRITE_DATA, // for backwared compatibility 37 | INVALID_DATA = 2, 38 | RESERVED = 3, 39 | /* Slave to Master */ 40 | READ_ACK = 4, 41 | WRITE_ACK = 5, 42 | DATA_INVALID = 6, 43 | UNKNOWN_DATA_ID = 7 44 | }; 45 | 46 | typedef OpenThermMessageType OpenThermRequestType; // for backwared compatibility 47 | 48 | enum OpenThermMessageID { 49 | // Master and Slave Status flags. 50 | MSG_STATUS = 0, 51 | // Control setpoint ie CH water temperature setpoint (°C) 52 | MSG_TSET = 1, 53 | // Master Configuration Flags / Master MemberID Code 54 | MSG_M_CONFIG_M_MEMBERIDCODE = 2, 55 | // Slave Configuration Flags / Slave MemberID Code 56 | MSG_S_CONFIG_S_MEMBERIDCODE = 3, 57 | // Remote Command 58 | MSG_COMMAND = 4, 59 | // Application-specific fault flags and OEM fault code 60 | MSG_ASF_FLAGS_OEM_FAULT_CODE = 5, 61 | // Remote boiler parameter transfer-enable & read/write flags 62 | MSG_RBP_FLAGS = 6, 63 | // Cooling control signal (%) 64 | MSG_COOLING_CONTROL = 7, 65 | // Control setpoint for 2nd CH circuit (°C) 66 | MSG_TSETCH2 = 8, 67 | // Remote override room setpoint 68 | MSG_TROVERRIDE = 9, 69 | // Number of Transparent-Slave-Parameters supported by slave 70 | MSG_TSP = 10, 71 | // Index number / Value of referred-to transparent slave parameter. 72 | MSG_TSP_INDEX_TSP_VALUE = 11, 73 | // Size of Fault-History-Buffer supported by slave 74 | MSG_FHB_SIZE = 12, 75 | // Index number / Value of referred-to fault-history buffer entry. 76 | MSG_FHB_INDEX_FHB_VALUE = 13, 77 | // Maximum relative modulation level setting (%) 78 | MSG_MAX_REL_MOD_LEVEL_SETTING = 14, 79 | // Maximum boiler capacity (kW) / Minimum boiler modulation level(%) 80 | MSG_MAX_CAPACITY_MIN_MOD_LEVEL = 15, 81 | // Room Setpoint (°C) 82 | MSG_TRSET = 16, 83 | // Relative Modulation Level (%) 84 | MSG_REL_MOD_LEVEL = 17, 85 | // Water pressure in CH circuit (bar) 86 | MSG_CH_PRESSURE = 18, 87 | // Water flow rate in DHW circuit. (litres/minute) 88 | MSG_DHW_FLOW_RATE = 19, 89 | // Day of Week and Time of Day 90 | MSG_DAY_TIME = 20, 91 | // Calendar date 92 | MSG_DATE = 21, 93 | // Calendar year 94 | MSG_YEAR = 22, 95 | // Room Setpoint for 2nd CH circuit (°C) 96 | MSG_TRSETCH2 = 23, 97 | // Room temperature (°C) 98 | MSG_TR = 24, 99 | // Boiler flow water temperature (°C) 100 | MSG_TBOILER = 25, 101 | // DHW temperature (°C) 102 | MSG_TDHW = 26, 103 | // Outside temperature (°C) 104 | MSG_TOUTSIDE = 27, 105 | // Return water temperature (°C) 106 | MSG_TRET = 28, 107 | // Solar storage temperature (°C) 108 | MSG_TSTORAGE = 29, 109 | // Solar collector temperature (°C) 110 | MSG_TCOLLECTOR = 30, 111 | // Flow water temperature CH2 circuit (°C) 112 | MSG_TFLOWCH2 = 31, 113 | // Domestic hot water temperature 2 (°C) 114 | MSG_TDHW2 = 32, 115 | // Boiler exhaust temperature (°C) 116 | MSG_TEXHAUST = 33, 117 | // DHW setpoint upper & lower bounds for adjustment (°C) 118 | MSG_TDHWSET_UB_LB = 48, 119 | // Max CH water setpoint upper & lower bounds for adjustment (°C) 120 | MSG_MAXTSET_UB_LB = 49, 121 | // OTC heat curve ratio upper & lower bounds for adjustment 122 | MSG_HCRATIO_UB_LB = 50, 123 | // DHW setpoint (°C) (Remote parameter 1) 124 | MSG_TDHWSET = 56, 125 | // Max CH water setpoint (°C) (Remote parameters 2) 126 | MSG_MAXTSET = 57, 127 | // OTC heat curve ratio (°C) (Remote parameter 3) 128 | MSG_HCRATIO = 58, 129 | // Function of manual and program changes in master and remote room setpoint. 130 | MSG_REMOTE_OVERRIDE_FUNCTION = 100, 131 | // OEM-specific diagnostic/service code 132 | MSG_OEM_DIAGNOSTIC_CODE = 115, 133 | // Number of starts burner 134 | MSG_BURNER_STARTS = 116, 135 | // Number of starts CH pump 136 | MSG_CH_PUMP_STARTS = 117, 137 | // Number of starts DHW pump/valve 138 | MSG_DHW_PUMP_VALVE_STARTS = 118, 139 | // Number of starts burner during DHW mode 140 | MSG_DHW_BURNER_STARTS = 119, 141 | // Number of hours that burner is in operation (i.e. flame on) 142 | MSG_BURNER_OPERATION_HOURS = 120, 143 | // Number of hours that CH pump has been running 144 | MSG_CH_PUMP_OPERATION_HOURS = 121, 145 | // Number of hours that DHW pump has been running or DHW valve has been opened 146 | MSG_DHW_PUMP_VALVE_OPERATION_HOURS = 122, 147 | // Number of hours that burner is in operation during DHW mode 148 | MSG_DHW_BURNER_OPERATION_HOURS = 123, 149 | // The implemented version of the OpenTherm Protocol Specification in the master. 150 | MSG_OPENTHERM_VERSION_MASTER = 124, 151 | // The implemented version of the OpenTherm Protocol Specification in the slave. 152 | MSG_OPENTHERM_VERSION_SLAVE = 125, 153 | // Master product version number and type 154 | MSG_MASTER_VERSION = 126, 155 | // Slave product version number and type 156 | MSG_SLAVE_VERSION = 127 157 | }; 158 | 159 | enum OpenThermStatus { 160 | NOT_INITIALIZED, 161 | READY, 162 | DELAY, 163 | REQUEST_SENDING, 164 | RESPONSE_WAITING, 165 | RESPONSE_START_BIT, 166 | RESPONSE_RECEIVING, 167 | RESPONSE_READY, 168 | RESPONSE_INVALID 169 | }; 170 | 171 | struct OpenThermStore { 172 | OpenThermStore(bool slave = false) 173 | : isSlave(slave) 174 | {} 175 | static void gpio_intr(OpenThermStore *arg); 176 | 177 | ISRInternalGPIOPin pin_in; 178 | volatile uint32_t response{0}; 179 | volatile uint32_t responseTimestamp{0}; 180 | volatile uint8_t responseBitIndex{0}; 181 | volatile OpenThermStatus status{OpenThermStatus::NOT_INITIALIZED}; 182 | const bool isSlave; 183 | }; 184 | 185 | class OpenThermChannel 186 | { 187 | public: 188 | OpenThermChannel(bool isSlave = false); 189 | ~OpenThermChannel(); 190 | 191 | void set_pin_in(InternalGPIOPin *pin_in) {this->pin_in_ = pin_in;} 192 | void set_pin_out(InternalGPIOPin *pin_out) {this->pin_out_ = pin_out;} 193 | 194 | void setup(std::function callback); 195 | void loop(); 196 | uint32_t sendRequest(uint32_t request); 197 | bool sendResponse(uint32_t request); 198 | OpenThermResponseStatus getLastResponseStatus(); 199 | 200 | protected: 201 | bool sendRequestAync(uint32_t request); 202 | bool isReady(); 203 | void setActiveState(); 204 | void setIdleState(); 205 | void activateBoiler(); 206 | void sendBit(bool high); 207 | 208 | std::function process_response_callback; 209 | InternalGPIOPin *pin_in_; 210 | InternalGPIOPin *pin_out_; 211 | const bool isSlave; 212 | OpenThermResponseStatus responseStatus; 213 | OpenThermStore store_; 214 | }; 215 | 216 | const char *statusToString(OpenThermResponseStatus status); 217 | uint32_t buildRequest(OpenThermMessageType type, OpenThermMessageID id, uint16_t data); 218 | uint32_t buildResponse(OpenThermMessageType type, OpenThermMessageID id, uint16_t data); 219 | bool parity(uint32_t frame); 220 | OpenThermMessageType getMessageType(uint32_t message); 221 | OpenThermMessageID getDataID(uint32_t frame); 222 | const char *messageTypeToString(OpenThermMessageType message_type); 223 | bool isValidRequest(uint32_t request); 224 | bool isValidResponse(uint32_t response); 225 | uint32_t modifyMsgData(uint32_t msg, uint16_t data); 226 | uint8_t getUBUInt8(const uint32_t response); 227 | uint8_t getLBUInt8(const uint32_t response); 228 | int8_t getUBInt8(const uint32_t response); 229 | int8_t getLBInt8(const uint32_t response); 230 | uint16_t getUInt16(const uint32_t response); 231 | int16_t getInt16(const uint32_t response); 232 | float getFloat(const uint32_t response); 233 | uint16_t temperatureToData(float temperature); 234 | 235 | } // namespace opentherm 236 | } // namespace esphome 237 | -------------------------------------------------------------------------------- /components/opentherm/opentherm_gw_climate.cpp: -------------------------------------------------------------------------------- 1 | #include "opentherm_gw_climate.h" 2 | #include "esphome/core/log.h" 3 | 4 | namespace esphome { 5 | namespace opentherm { 6 | 7 | static const char *TAG = "opentherm_gw.climate"; 8 | 9 | OpenThermGWClimate::OpenThermGWClimate() 10 | : mOT(), 11 | sOT(true) 12 | { 13 | } 14 | 15 | void OpenThermGWClimate::setup() { 16 | // restore set points 17 | auto restore = this->restore_state_(); 18 | if (restore.has_value()) { 19 | restore->to_call(this).perform(); 20 | } else { 21 | this->mode = climate::CLIMATE_MODE_AUTO; 22 | } 23 | 24 | mOT.setup(std::bind(&OpenThermGWClimate::processRequest, this, std::placeholders::_1, std::placeholders::_2)); 25 | sOT.setup(nullptr); 26 | } 27 | 28 | void OpenThermGWClimate::loop() 29 | { 30 | mOT.loop(); 31 | } 32 | 33 | void OpenThermGWClimate::control(const climate::ClimateCall &call) { 34 | if (call.get_mode().has_value()) 35 | this->mode = *call.get_mode(); 36 | if (call.get_target_temperature().has_value()) 37 | this->target_temperature = *call.get_target_temperature(); 38 | //if (call.get_away().has_value()) 39 | // this->away = *call.get_away(); 40 | 41 | // Set to 0 to pass along the value specified by the thermostat. To stop the boiler heating the house, 42 | // set the control setpoint to some low value and clear the CH enable bit using the CH command. 43 | 44 | this->publish_state(); 45 | } 46 | 47 | climate::ClimateTraits OpenThermGWClimate::traits() { 48 | auto traits = climate::ClimateTraits(); 49 | traits.set_supports_current_temperature(true); 50 | //traits.set_supports_auto_mode(true); 51 | // traits.set_supports_cool_mode(true); 52 | traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); 53 | traits.set_supported_presets({ 54 | climate::CLIMATE_PRESET_HOME, 55 | climate::CLIMATE_PRESET_AWAY, 56 | }); 57 | //traits.set_supports_action(true); 58 | return traits; 59 | } 60 | 61 | void OpenThermGWClimate::dump_config() { 62 | LOG_CLIMATE("", "OpenTherm Gateway Climate", this); 63 | // ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); 64 | } 65 | 66 | void OpenThermGWClimate::processRequest(uint32_t request, OpenThermResponseStatus status) { 67 | 68 | // master/thermostat request 69 | OpenThermMessageID id = getDataID(request); 70 | uint16_t data = getUInt16(request); 71 | //ESP_LOGD(TAG, "T %03d %04x", id, data); 72 | 73 | switch (id) { 74 | case MSG_DATE: 75 | process_Master_MSG_DATE(request); 76 | break; 77 | case MSG_DAY_TIME: 78 | process_Master_MSG_DAY_TIME(request); 79 | break; 80 | case MSG_YEAR: 81 | process_Master_MSG_YEAR(request); 82 | break; 83 | case MSG_COMMAND: 84 | process_Master_MSG_COMMAND(request); 85 | break; 86 | case MSG_MASTER_VERSION: 87 | process_Master_MSG_MASTER_VERSION(request); 88 | break; 89 | case MSG_M_CONFIG_M_MEMBERIDCODE: 90 | process_Master_MSG_M_CONFIG_M_MEMBERIDCODE(request); 91 | break; 92 | case MSG_OPENTHERM_VERSION_MASTER: 93 | process_Master_MSG_OPENTHERM_VERSION_MASTER(request); 94 | break; 95 | case MSG_STATUS: 96 | process_Master_MSG_STATUS(request); 97 | break; 98 | case MSG_TR: 99 | process_Master_MSG_TR(request); 100 | break; 101 | case MSG_TRSET: 102 | process_Master_MSG_TRSET(request); 103 | break; 104 | case MSG_TRSETCH2: 105 | process_Master_MSG_TRSETCH2(request); 106 | break; 107 | case MSG_TSET: 108 | process_Master_MSG_TSET(request); 109 | break; 110 | case MSG_TSETCH2: 111 | process_Master_MSG_TSETCH2(request); 112 | break; 113 | case MSG_TDHWSET: 114 | process_Master_MSG_TDHWSET(request); 115 | break; 116 | case MSG_MAXTSET: 117 | process_Master_MSG_MAXTSET(request); 118 | break; 119 | case MSG_TSP_INDEX_TSP_VALUE: 120 | process_Master_MSG_TSP_INDEX_TSP_VALUE(request); 121 | break; 122 | case MSG_COOLING_CONTROL: 123 | process_Master_MSG_COOLING_CONTROL(request); 124 | break; 125 | case MSG_MAX_REL_MOD_LEVEL_SETTING: 126 | process_Master_MSG_MAX_REL_MOD_LEVEL_SETTING(request); 127 | break; 128 | default: 129 | //ESP_LOGD(TAG, "Request %d not handled!", id); 130 | break; 131 | } 132 | 133 | uint32_t response = sOT.sendRequest(request); 134 | status = sOT.getLastResponseStatus(); 135 | processResponse(response, status); 136 | } 137 | 138 | void OpenThermGWClimate::processResponse(uint32_t &response, OpenThermResponseStatus status) { 139 | 140 | // slave/boiler response 141 | OpenThermMessageID id = getDataID(response); 142 | uint16_t data = getUInt16(response); 143 | //ESP_LOGD(TAG, "B %03d %04x", id, data); 144 | switch (id) { 145 | case MSG_BURNER_STARTS: 146 | process_Slave_MSG_BURNER_STARTS(response); 147 | break; 148 | case MSG_CH_PRESSURE: 149 | process_Slave_MSG_CH_PRESSURE(response); 150 | break; 151 | case MSG_CH_PUMP_OPERATION_HOURS: 152 | process_Slave_MSG_CH_PUMP_OPERATION_HOURS(response); 153 | break; 154 | case MSG_CH_PUMP_STARTS: 155 | process_Slave_MSG_CH_PUMP_STARTS(response); 156 | break; 157 | case MSG_COMMAND: 158 | process_Slave_MSG_COMMAND(response); 159 | break; 160 | case MSG_DHW_BURNER_OPERATION_HOURS: 161 | process_Slave_MSG_DHW_BURNER_OPERATION_HOURS(response); 162 | break; 163 | case MSG_DHW_BURNER_STARTS: 164 | process_Slave_MSG_DHW_BURNER_STARTS(response); 165 | break; 166 | case MSG_DHW_FLOW_RATE: 167 | process_Slave_MSG_DHW_FLOW_RATE(response); 168 | break; 169 | case MSG_DHW_PUMP_VALVE_OPERATION_HOURS: 170 | process_Slave_MSG_DHW_PUMP_VALVE_OPERATION_HOURS(response); 171 | break; 172 | case MSG_DHW_PUMP_VALVE_STARTS: 173 | process_Slave_MSG_DHW_PUMP_VALVE_STARTS(response); 174 | break; 175 | case MSG_BURNER_OPERATION_HOURS: 176 | process_Slave_MSG_BURNER_OPERATION_HOURS(response); 177 | break; 178 | case MSG_OPENTHERM_VERSION_SLAVE: 179 | process_Slave_MSG_OPENTHERM_VERSION_SLAVE(response); 180 | break; 181 | case MSG_REL_MOD_LEVEL: 182 | process_Slave_MSG_REL_MOD_LEVEL(response); 183 | break; 184 | case MSG_REMOTE_OVERRIDE_FUNCTION: 185 | process_Slave_MSG_REMOTE_OVERRIDE_FUNCTION(response); 186 | break; 187 | case MSG_SLAVE_VERSION: 188 | process_Slave_MSG_SLAVE_VERSION(response); 189 | break; 190 | case MSG_STATUS: 191 | process_Slave_MSG_STATUS(response); 192 | break; 193 | case MSG_S_CONFIG_S_MEMBERIDCODE: 194 | process_Slave_MSG_S_CONFIG_S_MEMBERIDCODE(response); 195 | break; 196 | case MSG_TBOILER: 197 | process_Slave_MSG_TBOILER(response); 198 | break; 199 | case MSG_TCOLLECTOR: 200 | process_Slave_MSG_TCOLLECTOR(response); 201 | break; 202 | case MSG_TDHW2: 203 | process_Slave_MSG_TDHW2(response); 204 | break; 205 | case MSG_TDHW: 206 | process_Slave_MSG_TDHW(response); 207 | break; 208 | case MSG_TEXHAUST: 209 | process_Slave_MSG_TEXHAUST(response); 210 | break; 211 | case MSG_TFLOWCH2: 212 | process_Slave_MSG_TFLOWCH2(response); 213 | break; 214 | case MSG_TOUTSIDE: 215 | process_Slave_MSG_TOUTSIDE(response); 216 | break; 217 | case MSG_TRET: 218 | process_Slave_MSG_TRET(response); 219 | break; 220 | case MSG_TROVERRIDE: 221 | process_Slave_MSG_TROVERRIDE(response); 222 | break; 223 | case MSG_TSTORAGE: 224 | process_Slave_MSG_TSTORAGE(response); 225 | break; 226 | case MSG_DATE: 227 | process_Slave_MSG_DATE(response); 228 | break; 229 | case MSG_DAY_TIME: 230 | process_Slave_MSG_DAY_TIME(response); 231 | break; 232 | case MSG_YEAR: 233 | process_Slave_MSG_YEAR(response); 234 | break; 235 | case MSG_ASF_FLAGS_OEM_FAULT_CODE: 236 | process_Slave_MSG_ASF_FLAGS_OEM_FAULT_CODE(response); 237 | break; 238 | case MSG_RBP_FLAGS: 239 | process_Slave_MSG_RBP_FLAGS(response); 240 | break; 241 | case MSG_TDHWSET_UB_LB: 242 | process_Slave_MSG_TDHWSET_UB_LB(response); 243 | break; 244 | case MSG_MAXTSET_UB_LB: 245 | process_Slave_MSG_MAXTSET_UB_LB(response); 246 | break; 247 | case MSG_TDHWSET: 248 | process_Slave_MSG_TDHWSET(response); 249 | break; 250 | case MSG_MAXTSET: 251 | process_Slave_MSG_MAXTSET(response); 252 | break; 253 | case MSG_TSP: 254 | process_Slave_MSG_TSP(response); 255 | break; 256 | case MSG_TSP_INDEX_TSP_VALUE: 257 | process_Slave_MSG_TSP_INDEX_TSP_VALUE(response); 258 | break; 259 | case MSG_FHB_SIZE: 260 | process_Slave_MSG_FHB_SIZE(response); 261 | break; 262 | case MSG_FHB_INDEX_FHB_VALUE: 263 | process_Slave_MSG_FHB_INDEX_FHB_VALUE(response); 264 | break; 265 | case MSG_MAX_CAPACITY_MIN_MOD_LEVEL: 266 | process_Slave_MSG_MAX_CAPACITY_MIN_MOD_LEVEL(response); 267 | break; 268 | case MSG_OEM_DIAGNOSTIC_CODE: 269 | process_Slave_MSG_OEM_DIAGNOSTIC_CODE(response); 270 | break; 271 | default: 272 | //ESP_LOGD(TAG, "Response %d not handled!", id); 273 | break; 274 | } 275 | 276 | mOT.sendResponse(response); 277 | } 278 | 279 | 280 | /** Class 1 : Control and Status Information **/ 281 | // This group of data-items contains important control and status information relating to the slave and master. 282 | // The slave status contains a mandatory fault-indication flag and there is an optional application-specific set of 283 | // fault flags which relate to specific faults in boiler-related applications, and an OEM fault code whose meaning 284 | // is unknown to the master but can be used for display purposes. 285 | 286 | // #0: Status 287 | // The gateway may want to manipulate the status message generated by the 288 | // thermostat for two reasons: The domestic hot water enable option specified 289 | // via a serial command may differ from what the thermostat sent. And if a 290 | // control setpoint has been specified via a serial command, central heating 291 | // mode must be enabled for the setpoint to have any effect. 292 | // Also if the master status byte was changed before sending it to the 293 | // boiler, the original master status must be restored before the message is 294 | // returned to the thermostat, so it doesn't get confused 295 | void OpenThermGWClimate::process_Master_MSG_STATUS(uint32_t &request) { 296 | uint8_t ub = getUBUInt8(request); 297 | 298 | // The CH enabled bit has priority over the Control Setpoint. The master can indicate that no CH demand is 299 | // required by putting the CH enabled bit = 0 (ie CH is disabled), even if the Control Setpoint is non-zero. 300 | bool master_ch_enabled = ub & (1 << 0); 301 | bool master_dhw_enabled = ub & (1 << 1); 302 | bool master_cooling_enabled = ub & (1 << 2); 303 | bool master_otc_enabled = ub & (1 << 3); 304 | bool master_ch2_enabled = ub & (1 << 4); 305 | 306 | //ESP_LOGD(TAG, "master_ch_enabled: %s", YESNO(master_ch_enabled)); 307 | //ESP_LOGD(TAG, "master_dhw_enabled: %s", YESNO(master_dhw_enabled)); 308 | //ESP_LOGD(TAG, "master_cooling_enabled: %s", YESNO(master_cooling_enabled)); 309 | //ESP_LOGD(TAG, "master_otc_enabled: %s", YESNO(master_otc_enabled)); 310 | //ESP_LOGD(TAG, "master_ch2_enabled: %s", YESNO(master_ch2_enabled)); 311 | 312 | //if (this->away) { 313 | 314 | //} 315 | } 316 | 317 | void OpenThermGWClimate::process_Slave_MSG_STATUS(uint32_t &response) { 318 | uint8_t lb = getLBUInt8(response); 319 | 320 | bool slave_fault_indication = lb & (1 << 0); 321 | bool slave_ch_active = lb & (1 << 1); 322 | bool slave_dhw_active = lb & (1 << 2); 323 | bool slave_flame_on = lb & (1 << 3); 324 | bool slave_cooling_active = lb & (1 << 4); 325 | bool slave_ch2_active = lb & (1 << 5); 326 | bool slave_diagnostic_event = lb & (1 << 6); 327 | 328 | //ESP_LOGD(TAG, "slave_fault_indication: %s", YESNO(slave_fault_indication)); 329 | //ESP_LOGD(TAG, "slave_ch_active: %s", YESNO(slave_ch_active)); 330 | //ESP_LOGD(TAG, "slave_dhw_active: %s", YESNO(slave_dhw_active)); 331 | //ESP_LOGD(TAG, "slave_flame_on: %s", YESNO(slave_flame_on)); 332 | //ESP_LOGD(TAG, "slave_cooling_active: %s", YESNO(slave_cooling_active)); 333 | //ESP_LOGD(TAG, "slave_ch2_active: %s", YESNO(slave_ch2_active)); 334 | //ESP_LOGD(TAG, "slave_diagnostic_event: %s", YESNO(slave_diagnostic_event)); 335 | 336 | if (this->is_fault_indication != nullptr) { 337 | this->is_fault_indication->publish_state(slave_fault_indication); 338 | } 339 | if (this->is_ch_active != nullptr) { 340 | this->is_ch_active->publish_state(slave_ch_active); 341 | } 342 | if (this->is_dhw_active != nullptr) { 343 | this->is_dhw_active->publish_state(slave_dhw_active); 344 | } 345 | if (this->is_flame_on != nullptr) { 346 | this->is_flame_on->publish_state(slave_flame_on); 347 | } 348 | if (this->is_cooling_active != nullptr) { 349 | this->is_cooling_active->publish_state(slave_cooling_active); 350 | } 351 | if (this->is_ch2_active != nullptr) { 352 | this->is_ch2_active->publish_state(slave_ch2_active); 353 | } 354 | if (this->is_diagnostic_event != nullptr) { 355 | this->is_diagnostic_event->publish_state(slave_diagnostic_event); 356 | } 357 | 358 | if (slave_fault_indication) { 359 | // Remember a fault was reported 360 | // ASF-flags/OEM-fault-code message 361 | // Schedule request for more information 362 | 363 | } 364 | } 365 | 366 | // #1: Control setpoint ie CH water temperature setpoint (°C) 367 | void OpenThermGWClimate::process_Master_MSG_TSET(uint32_t &request) { 368 | float control_setpoint = getFloat(request); 369 | ESP_LOGD(TAG, "CH water temperature setpoint (°C): %f", control_setpoint); 370 | if (control_setpoint != this->target_temperature) { 371 | //request = modifyMsgData(request, temperatureToData(this->target_temperature)); 372 | } 373 | } 374 | 375 | // #5: Application-specific flags 376 | void OpenThermGWClimate::process_Slave_MSG_ASF_FLAGS_OEM_FAULT_CODE(uint32_t &response) { 377 | uint8_t ub = getUBUInt8(response); 378 | uint8_t oem_fault_code = getLBUInt8(response); 379 | 380 | bool slave_service_request = ub & (1 << 0); // service not req’d, service required 381 | bool slave_lockout_reset = ub & (1 << 1); // remote reset disabled, rr enabled 382 | bool slave_low_water_press = ub & (1 << 2); // no WP fault, water pressure fault 383 | bool slave_gas_flame_fault = ub & (1 << 3); // no G/F fault, gas/flame fault 384 | bool slave_air_press_fault = ub & (1 << 4); // no AP fault, air pressure fault 385 | bool slave_water_over_temp = ub & (1 << 5); // no OvT fault, over-temperature fault 386 | } 387 | 388 | // #8: Control setpoint for 2nd CH circuit (°C) 389 | void OpenThermGWClimate::process_Master_MSG_TSETCH2(uint32_t &request) { 390 | // The master decides the actual range over which the control setpoint is defined. The default range is 391 | // assumed to be 0 to 100. 392 | /*if (temperature < 0.0f) 393 | temperature = 0.0f; 394 | if (temperature > 100.0f) 395 | temperature = 100.0f;*/ 396 | float control_setpoint = getFloat(request); 397 | ESP_LOGD(TAG, "Control setpoint for 2nd CH circuit (°C): %f", control_setpoint); 398 | } 399 | 400 | // #115: OEM diagnostic code 401 | void OpenThermGWClimate::process_Slave_MSG_OEM_DIAGNOSTIC_CODE(uint32_t &request) { 402 | uint16_t oem_diagnostic_code = getUInt16(request); 403 | ESP_LOGD(TAG, "oem_diagnostic_code: %d", oem_diagnostic_code); 404 | } 405 | 406 | /** Class 2 : Configuration Information **/ 407 | // This group of data-items defines configuration information on both the slave and master sides. Each has a 408 | // group of configuration flags (8 bits) and an MemberID code (1 byte). A valid Read Slave Configuration and 409 | // Write Master Configuration message exchange is recommended before control and status information is 410 | // transmitted. 411 | 412 | // #2: Master configuration & Master MemberID code 413 | void OpenThermGWClimate::process_Master_MSG_M_CONFIG_M_MEMBERIDCODE(uint32_t &response) { 414 | uint8_t master_configuration = getUBUInt8(response); 415 | 416 | // Remeha = 11 417 | uint8_t master_memberid_code = getLBUInt8(response); 418 | ESP_LOGD(TAG, "master_memberid_code: %d", master_memberid_code); 419 | } 420 | 421 | // #3: Slave configuration & Slave MemberID code 422 | void OpenThermGWClimate::process_Slave_MSG_S_CONFIG_S_MEMBERIDCODE(uint32_t &request) { 423 | uint8_t slave_configuration = getUBUInt8(request); 424 | uint8_t slave_member_id_code = getLBUInt8(request); 425 | 426 | bool slave_dhw_present = slave_configuration & (1 << 0); // dhw not present, dhw is present 427 | bool slave_control_type = slave_configuration & (1 << 1); // modulating, on/off 428 | bool slave_cooling_config = slave_configuration & (1 << 2); // cooling not supported, cooling supported 429 | bool slave_dhw_config = slave_configuration & (1 << 3); // instantaneous or not-specified, storage tank 430 | bool slave_low_off_pump_control_function = slave_configuration & (1 << 4); // allowed, not allowed 431 | bool slave_ch2_present = slave_configuration & (1 << 5); // CH2 not present, CH2 present 432 | 433 | ESP_LOGD(TAG, "slave_dhw_present: %s", YESNO(slave_dhw_present)); 434 | ESP_LOGD(TAG, "slave_control_type: %s", YESNO(slave_control_type)); 435 | ESP_LOGD(TAG, "slave_cooling_config: %s", YESNO(slave_cooling_config)); 436 | ESP_LOGD(TAG, "slave_dhw_config: %s", YESNO(slave_dhw_config)); 437 | ESP_LOGD(TAG, "slave_low_off_pump_control_function: %s", YESNO(slave_low_off_pump_control_function)); 438 | ESP_LOGD(TAG, "slave_ch2_present: %s", YESNO(slave_ch2_present)); 439 | } 440 | 441 | // #124: OpenTherm version Master 442 | void OpenThermGWClimate::process_Master_MSG_OPENTHERM_VERSION_MASTER(uint32_t &response) { 443 | uint16_t opentherm_version_master = getUInt16(response); 444 | ESP_LOGD(TAG, "opentherm_version_master: %d", opentherm_version_master); 445 | } 446 | 447 | // #125: OpenTherm version Slave 448 | void OpenThermGWClimate::process_Slave_MSG_OPENTHERM_VERSION_SLAVE(uint32_t &request) { 449 | uint16_t opentherm_version_slave = getUInt16(request); 450 | ESP_LOGD(TAG, "opentherm_version_slave: %d", opentherm_version_slave); 451 | } 452 | 453 | // #126: Master product version number and type 454 | void OpenThermGWClimate::process_Master_MSG_MASTER_VERSION(uint32_t &request) { 455 | uint8_t product_type = getUBUInt8(request); 456 | uint8_t product_version = getLBUInt8(request); 457 | ESP_LOGD(TAG, "master product type/version: %d/%d", product_type, product_version); 458 | } 459 | 460 | // #127: Slave product version number and type 461 | void OpenThermGWClimate::process_Slave_MSG_SLAVE_VERSION(uint32_t &response) { 462 | uint8_t product_type = getUBUInt8(response); 463 | uint8_t product_version = getLBUInt8(response); 464 | ESP_LOGD(TAG, "slave product type/version: %d/%d", product_type, product_version); 465 | } 466 | 467 | /* Class 3 : Remote Commands */ 468 | // This class of data represents commands sent by the master to the slave. There is a single data-id for a 469 | // command “packet”, with the Command-Code embedded in the high-byte of the data-value field. 470 | 471 | // #4: HB: Command-Code 472 | void OpenThermGWClimate::process_Master_MSG_COMMAND(uint32_t &request) { 473 | uint8_t command_code = getUBUInt8(request); 474 | ESP_LOGD(TAG, "master command code: %d", command_code); 475 | } 476 | 477 | // #4: Cmd-Response-Code 478 | void OpenThermGWClimate::process_Slave_MSG_COMMAND(uint32_t &response) { 479 | uint8_t product_version = getLBUInt8(response); 480 | ESP_LOGD(TAG, "slave product_version: %d", product_version); 481 | } 482 | 483 | /* Class 4 : Sensor and Informational Data */ 484 | // This group of data-items contains sensor data (temperatures, pressures etc.) and other informational data 485 | // from one unit to the other. 486 | 487 | // #16: Room Setpoint 488 | void OpenThermGWClimate::process_Master_MSG_TRSET(uint32_t &request) { 489 | float room_setpoint = getFloat(request); 490 | ESP_LOGD(TAG, "room_setpoint: %f", room_setpoint); 491 | if (this->target_temperature != room_setpoint) { 492 | this->target_temperature = room_setpoint; 493 | this->publish_state(); 494 | } 495 | } 496 | 497 | // #17: Relative Modulation Level 498 | void OpenThermGWClimate::process_Slave_MSG_REL_MOD_LEVEL(uint32_t &response) { 499 | float relative_modulation_level = getFloat(response); 500 | ESP_LOGD(TAG, "relative_modulation_level: %f", relative_modulation_level); 501 | if (this->relative_modulation_level != nullptr) { 502 | this->relative_modulation_level->publish_state(relative_modulation_level); 503 | } 504 | } 505 | 506 | // #18: Water pressure of the boiler CH circuit (bar) 507 | void OpenThermGWClimate::process_Slave_MSG_CH_PRESSURE(uint32_t &response) { 508 | float ch_water_pressure = getFloat(response); 509 | ESP_LOGD(TAG, "ch_water_pressure: %f bar", ch_water_pressure); 510 | if (this->ch_water_pressure != nullptr) { 511 | this->ch_water_pressure->publish_state(ch_water_pressure); 512 | } 513 | } 514 | 515 | // #19: Water flow rate through the DHW circuit (l/min) 516 | void OpenThermGWClimate::process_Slave_MSG_DHW_FLOW_RATE(uint32_t &response) { 517 | float dhw_flow_rate = getFloat(response); 518 | ESP_LOGD(TAG, "dhw_flow_rate: %f l/min", dhw_flow_rate); 519 | if (this->dhw_flow_rate != nullptr) { 520 | this->dhw_flow_rate->publish_state(dhw_flow_rate); 521 | } 522 | } 523 | 524 | // #20: Day of Week & Time of Day 525 | void OpenThermGWClimate::process_Master_MSG_DAY_TIME(uint32_t &msg) { 526 | uint8_t ub = getUBUInt8(msg); 527 | uint8_t minutes = getLBUInt8(msg); 528 | uint8_t day_of_week = (ub >> 5) & 7; 529 | uint8_t hours = (ub & 0x1f); 530 | } 531 | 532 | void OpenThermGWClimate::process_Slave_MSG_DAY_TIME(uint32_t &msg) { 533 | uint8_t ub = getUBUInt8(msg); 534 | uint8_t minutes = getLBUInt8(msg); 535 | uint8_t day_of_week = (ub >> 5) & 7; 536 | uint8_t hours = (ub & 0x1f); 537 | } 538 | 539 | // #21: Date 540 | void OpenThermGWClimate::process_Master_MSG_DATE(uint32_t &msg) { 541 | uint8_t month = getUBUInt8(msg); 542 | uint8_t day_of_month = getLBUInt8(msg); 543 | } 544 | 545 | void OpenThermGWClimate::process_Slave_MSG_DATE(uint32_t &msg) { 546 | uint8_t month = getUBUInt8(msg); 547 | uint8_t day_of_month = getLBUInt8(msg); 548 | } 549 | 550 | // #22: Year 551 | void OpenThermGWClimate::process_Master_MSG_YEAR(uint32_t &msg) { 552 | uint16_t year = getUInt16(msg); 553 | } 554 | 555 | void OpenThermGWClimate::process_Slave_MSG_YEAR(uint32_t &msg) { 556 | uint16_t year = getUInt16(msg); 557 | } 558 | 559 | // #23: Current room setpoint for 2nd CH circuit (°C) 560 | void OpenThermGWClimate::process_Master_MSG_TRSETCH2(uint32_t &request) { 561 | float room_setpoint_ch2 = getFloat(request); 562 | ESP_LOGD(TAG, "room_setpoint_ch2: %f", room_setpoint_ch2); 563 | } 564 | 565 | // #24: Current sensed room temperature (°C) 566 | void OpenThermGWClimate::process_Master_MSG_TR(uint32_t &request) { 567 | float room_temperature = getFloat(request); 568 | ESP_LOGD(TAG, "room_temperature: %f", room_temperature); 569 | if (this->current_temperature != room_temperature) { 570 | this->current_temperature = room_temperature; 571 | this->publish_state(); 572 | } 573 | } 574 | 575 | // #25: Flow water temperature from boiler (°C) 576 | void OpenThermGWClimate::process_Slave_MSG_TBOILER(uint32_t &response) { 577 | float boiler_water_temp = getFloat(response); 578 | ESP_LOGD(TAG, "boiler_water_temp: %f", boiler_water_temp); 579 | if (this->boiler_water_temp != nullptr) { 580 | this->boiler_water_temp->publish_state(boiler_water_temp); 581 | } 582 | } 583 | 584 | // #26: Domestic hot water temperature (°C) 585 | void OpenThermGWClimate::process_Slave_MSG_TDHW(uint32_t &response) { 586 | float dhw_temperature = getFloat(response); 587 | ESP_LOGD(TAG, "dhw_temperature: %f", dhw_temperature); 588 | if (this->dhw_temperature != nullptr) { 589 | this->dhw_temperature->publish_state(dhw_temperature); 590 | } 591 | } 592 | 593 | // #27: Outside air temperature (°C) 594 | void OpenThermGWClimate::process_Slave_MSG_TOUTSIDE(uint32_t &response) { 595 | float outside_air_temperature = getFloat(response); 596 | ESP_LOGD(TAG, "outside_air_temperature: %f", outside_air_temperature); 597 | if (this->outside_air_temperature != nullptr) { 598 | this->outside_air_temperature->publish_state(outside_air_temperature); 599 | } 600 | } 601 | 602 | // #28: Return water temperature to boiler (°C) 603 | void OpenThermGWClimate::process_Slave_MSG_TRET(uint32_t &response) { 604 | float return_water_temperature = getFloat(response); 605 | ESP_LOGD(TAG, "return_water_temperature: %f", return_water_temperature); 606 | if (this->return_water_temperature != nullptr) { 607 | this->return_water_temperature->publish_state(return_water_temperature); 608 | } 609 | } 610 | 611 | // #29: Solar storage temperature (°C) 612 | void OpenThermGWClimate::process_Slave_MSG_TSTORAGE(uint32_t &response) { 613 | float solar_storage_temperature = getFloat(response); 614 | ESP_LOGD(TAG, "solar_storage_temperature: %f", solar_storage_temperature); 615 | if (this->solar_storage_temperature != nullptr) { 616 | this->solar_storage_temperature->publish_state(solar_storage_temperature); 617 | } 618 | } 619 | 620 | // #30: Solar collector temperature (°C) 621 | void OpenThermGWClimate::process_Slave_MSG_TCOLLECTOR(uint32_t &response) { 622 | int16_t solar_collector_temperature = getInt16(response); 623 | ESP_LOGD(TAG, "solar_collector_temperature: %d", solar_collector_temperature); 624 | if (this->solar_collector_temperature != nullptr) { 625 | this->solar_collector_temperature->publish_state(solar_collector_temperature); 626 | } 627 | } 628 | 629 | // #31: Flow water temperature of the second central 630 | void OpenThermGWClimate::process_Slave_MSG_TFLOWCH2(uint32_t &response) { 631 | float flow_temperature_ch2 = getFloat(response); 632 | ESP_LOGD(TAG, "flow_temperature_ch2: %f", flow_temperature_ch2); 633 | if (this->flow_temperature_ch2 != nullptr) { 634 | this->flow_temperature_ch2->publish_state(flow_temperature_ch2); 635 | } 636 | } 637 | 638 | // #32: Domestic hot water temperature 2 (°C) 639 | void OpenThermGWClimate::process_Slave_MSG_TDHW2(uint32_t &response) { 640 | float dhw2_temperature = getFloat(response); 641 | ESP_LOGD(TAG, "dhw2_temperature: %f", dhw2_temperature); 642 | if (this->dhw2_temperature != nullptr) { 643 | this->dhw2_temperature->publish_state(dhw2_temperature); 644 | } 645 | } 646 | 647 | // #33: Exhaust temperature (°C) 648 | void OpenThermGWClimate::process_Slave_MSG_TEXHAUST(uint32_t &response) { 649 | int16_t exhaust_temperature = getInt16(response); 650 | ESP_LOGD(TAG, "exhaust_temperature: %d", exhaust_temperature); 651 | if (this->exhaust_temperature != nullptr) { 652 | this->exhaust_temperature->publish_state(exhaust_temperature); 653 | } 654 | } 655 | 656 | // #116: Number of starts burner. Reset by writing zero is optional for slave. 657 | void OpenThermGWClimate::process_Slave_MSG_BURNER_STARTS(uint32_t &response) { 658 | uint16_t burner_starts = getUInt16(response); 659 | ESP_LOGD(TAG, "burner_starts: %d", burner_starts); 660 | if (this->burner_starts != nullptr) { 661 | this->burner_starts->publish_state(burner_starts); 662 | } 663 | } 664 | 665 | // #117: Number of starts CH pump. Reset by writing zero is optional for slave. 666 | void OpenThermGWClimate::process_Slave_MSG_CH_PUMP_STARTS(uint32_t &response) { 667 | uint16_t ch_pump_starts = getUInt16(response); 668 | ESP_LOGD(TAG, "ch_pump_starts: %d", ch_pump_starts); 669 | if (this->ch_pump_starts != nullptr) { 670 | this->ch_pump_starts->publish_state(ch_pump_starts); 671 | } 672 | } 673 | 674 | // #118: Number of starts DHW pump/valve. Reset by writing zero is optional for slave. 675 | void OpenThermGWClimate::process_Slave_MSG_DHW_PUMP_VALVE_STARTS(uint32_t &response) { 676 | uint16_t dhw_pump_valve_starts = getUInt16(response); 677 | ESP_LOGD(TAG, "dhw_pump_valve_starts: %d", dhw_pump_valve_starts); 678 | if (this->dhw_pump_valve_starts != nullptr) { 679 | this->dhw_pump_valve_starts->publish_state(dhw_pump_valve_starts); 680 | } 681 | } 682 | 683 | // #119: Number of starts burner in DHW mode. Reset by writing zero is optional for slave. 684 | void OpenThermGWClimate::process_Slave_MSG_DHW_BURNER_STARTS(uint32_t &response) { 685 | uint16_t dhw_burner_starts = getUInt16(response); 686 | ESP_LOGD(TAG, "dhw_burner_starts: %d", dhw_burner_starts); 687 | if (this->dhw_burner_starts != nullptr) { 688 | this->dhw_burner_starts->publish_state(dhw_burner_starts); 689 | } 690 | } 691 | 692 | // #120: Number of hours that burner is in operation (i.e. flame on). Reset by writing zero is optional for slave. 693 | void OpenThermGWClimate::process_Slave_MSG_BURNER_OPERATION_HOURS(uint32_t &response) { 694 | uint16_t burner_operation_hours = getUInt16(response); 695 | ESP_LOGD(TAG, "burner_operation_hours: %d", burner_operation_hours); 696 | if (this->burner_operation_hours != nullptr) { 697 | this->burner_operation_hours->publish_state(burner_operation_hours); 698 | } 699 | } 700 | 701 | // #121: Number of hours that CH pump has been running. Reset by writing zero is optional for slave. 702 | void OpenThermGWClimate::process_Slave_MSG_CH_PUMP_OPERATION_HOURS(uint32_t &response) { 703 | uint16_t ch_pump_operation_hours = getUInt16(response); 704 | ESP_LOGD(TAG, "ch_pump_operation_hours: %d", ch_pump_operation_hours); 705 | if (this->ch_pump_operation_hours != nullptr) { 706 | this->ch_pump_operation_hours->publish_state(ch_pump_operation_hours); 707 | } 708 | } 709 | 710 | // #122: Number of hours that DHW pump has been running or DHW valve has been opened. Reset by writing zero is optional for slave. 711 | void OpenThermGWClimate::process_Slave_MSG_DHW_PUMP_VALVE_OPERATION_HOURS(uint32_t &response) { 712 | uint16_t dhw_pump_valve_operation_hours = getUInt16(response); 713 | ESP_LOGD(TAG, "dhw_pump_valve_operation_hours: %d", dhw_pump_valve_operation_hours); 714 | if (this->dhw_pump_valve_operation_hours != nullptr) { 715 | this->dhw_pump_valve_operation_hours->publish_state(dhw_pump_valve_operation_hours); 716 | } 717 | } 718 | 719 | // #123: Number of hours that burner is in operation during DHW mode. Reset by writing zero is optional for slave. 720 | void OpenThermGWClimate::process_Slave_MSG_DHW_BURNER_OPERATION_HOURS(uint32_t &response) { 721 | uint16_t dhw_burner_operation_hours = getUInt16(response); 722 | ESP_LOGD(TAG, "dhw_burner_operation_hours: %d", dhw_burner_operation_hours); 723 | if (this->dhw_burner_operation_hours != nullptr) { 724 | this->dhw_burner_operation_hours->publish_state(dhw_burner_operation_hours); 725 | } 726 | } 727 | 728 | /* Class 5 : Pre-Defined Remote Boiler Parameters */ 729 | // This group of data-items defines specific parameters of the slave device (setpoints, etc.) which may be 730 | // available to the master device and may, or may not, be adjusted remotely. These parameters are 731 | // prespecified in the protocol and are specifically related to boiler/room controller applications. There is a 732 | // maximum of 8 remote boiler parameters. Each remote-boiler-parameter has a upper- and lower-bound (max 733 | // and min values) which the master should read from the slave in order to make sure they are not set outside 734 | // the valid range. If the slave does not support sending the upper- and lower-bounds, the master can apply 735 | // default bounds as it chooses. 736 | 737 | // The remote-parameter transfer-enable flags indicate which remote parameters are supported by the slave. 738 | // The remote-parameter read/write flags indicate whether the master can only read the parameter from the 739 | // slave, or whether it can also modify the parameter and write it back to the slave. An Unknown Data-Id 740 | // response to a Read Remote-Parameter-Flags message indicates no support for remote-parameters 741 | // (equivalent to all transfer-enable flags equal to zero). In these flag bytes bit 0 corresponds to remote-boiler 742 | // parameter 1 and bit 7 to remote-boiler-parameter 8. 743 | 744 | // #6: Remote-parameter 745 | void OpenThermGWClimate::process_Slave_MSG_RBP_FLAGS(uint32_t &response) { 746 | uint8_t ub = getUBUInt8(response); 747 | uint8_t lb = getLBUInt8(response); 748 | 749 | bool dhw_setpoint_enabled = ub & (1 << 0); // DHW setpoint 750 | bool max_ch_setpoint_enabled = ub & (1 << 1); // max CHsetpoint 751 | 752 | bool dhw_setpoint_readonly = lb & (1 << 0); // DHW setpoint read-only? 753 | bool max_ch_setpoint_readonly = lb & (1 << 1); // max CHsetpoint read-only? 754 | 755 | ESP_LOGD(TAG, "slave dhw enabled: %d, max_ch_setpoint_enabled: %d", dhw_setpoint_enabled, max_ch_setpoint_enabled); 756 | } 757 | 758 | // #48: DHW setpoint upper & lower bounds for adjustment (°C) 759 | void OpenThermGWClimate::process_Slave_MSG_TDHWSET_UB_LB(uint32_t &response) { 760 | int8_t dhw_setpoint_ub = getUBInt8(response); // Upper bound for adjustment of DHW setpoint (°C) 761 | int8_t dhw_setpoint_lb = getLBInt8(response); // Lower bound for adjustment of DHW setpoint (°C) 762 | ESP_LOGD(TAG, "dhw_setpoint_ub: %d, dhw_setpoint_lb: %d", dhw_setpoint_ub, dhw_setpoint_lb); 763 | } 764 | 765 | // #49: Max CH water setpoint upper & lower bounds for adjustment (°C) 766 | void OpenThermGWClimate::process_Slave_MSG_MAXTSET_UB_LB(uint32_t &response) { 767 | int8_t max_ch_setpoint_ub = getUBInt8(response); // Upper bound for adjustment of maxCHsetp (°C) 768 | int8_t max_ch_setpoint_lb = getLBInt8(response); // Lower bound for adjustment of maxCHsetp (°C) 769 | ESP_LOGD(TAG, "max_ch_setpoint_ub: %d, max_ch_setpoint_lb: %d", max_ch_setpoint_ub, max_ch_setpoint_lb); 770 | } 771 | 772 | // #56: DHW setpoint (°C) (Remote parameter 1) 773 | void OpenThermGWClimate::process_Master_MSG_TDHWSET(uint32_t &request) { 774 | float dhw_setpoint = getFloat(request); // Domestic hot water temperature setpoint (°C) 775 | ESP_LOGD(TAG, "master dhw_setpoint: %f", dhw_setpoint); 776 | } 777 | 778 | // #56: DHW setpoint (°C) (Remote parameter 1) 779 | void OpenThermGWClimate::process_Slave_MSG_TDHWSET(uint32_t &request) { 780 | float dhw_setpoint = getFloat(request); // Domestic hot water temperature setpoint (°C) 781 | ESP_LOGD(TAG, "slave dhw_setpoint: %f", dhw_setpoint); 782 | } 783 | 784 | // #57: Current room setpoint for 2nd CH circuit (°C) 785 | void OpenThermGWClimate::process_Master_MSG_MAXTSET(uint32_t &request) { 786 | float max_ch_water_setpoint = getFloat(request); // Maximum allowable CH water setpoint (°C) 787 | ESP_LOGD(TAG, "master max_ch_water_setpoint: %f", max_ch_water_setpoint); 788 | } 789 | 790 | // #57: Current room setpoint for 2nd CH circuit (°C) 791 | void OpenThermGWClimate::process_Slave_MSG_MAXTSET(uint32_t &request) { 792 | float max_ch_water_setpoint = getFloat(request); // Maximum allowable CH water setpoint (°C) 793 | ESP_LOGD(TAG, "slave max_ch_water_setpoint: %f", max_ch_water_setpoint); 794 | } 795 | 796 | /* Class 6 : Transparent Slave Parameters */ 797 | // This group of data-items defines parameters of the slave device which may (or may not) be remotely set by 798 | // the master device. These parameters are not pre-specified in the protocol and are “transparent” to the master 799 | // in the sense that it has no knowledge about their application meaning. 800 | 801 | // The first data-item (id=10) allows the master to read the number of transparent-slave-parameters supported 802 | // by the slave. The second data-item (ID=11) allows the master to read and write individual transparent-slave 803 | // parameters from/to the slave. 804 | 805 | // #10: Number of Transparent-Slave-Parameters supported by slave 806 | void OpenThermGWClimate::process_Slave_MSG_TSP(uint32_t &response) { 807 | uint8_t number_of_tsp = getUBUInt8(response); // Number of transparent-slave-parameter supported by the slave device. 808 | ESP_LOGD(TAG, "number_of_tsp: %d", number_of_tsp); 809 | } 810 | 811 | // #11: Index number / Value of referred-to transparent slave parameter. 812 | void OpenThermGWClimate::process_Master_MSG_TSP_INDEX_TSP_VALUE(uint32_t &response) { 813 | uint8_t tsp_index_no = getUBUInt8(response); // Index number of following TSP 814 | uint8_t tsp_value = getLBUInt8(response); // Value of above referenced TSP 815 | ESP_LOGD(TAG, "tsp_index_no: %d, tsp_index_no: %d", tsp_index_no, tsp_index_no); 816 | } 817 | 818 | // #11: Index number / Value of referred-to transparent slave parameter. 819 | void OpenThermGWClimate::process_Slave_MSG_TSP_INDEX_TSP_VALUE(uint32_t &response) { 820 | uint8_t tsp_index_no = getUBUInt8(response); // Index number of following TSP 821 | uint8_t tsp_value = getLBUInt8(response); // Value of above referenced TSP 822 | ESP_LOGD(TAG, "tsp_index_no: %d, tsp_index_no: %d", tsp_index_no, tsp_index_no); 823 | } 824 | 825 | /* Class 7 : Fault History Data */ 826 | // This group of data-items contains information relating to the past fault condition of the slave device. 827 | 828 | // #12: Size of Fault-History-Buffer supported by slave 829 | void OpenThermGWClimate::process_Slave_MSG_FHB_SIZE(uint32_t &response) { 830 | uint8_t size_of_fault_buffer = getUBUInt8(response); // The size of the fault history buffer. 831 | ESP_LOGD(TAG, "size_of_fault_buffer: %d", size_of_fault_buffer); 832 | } 833 | 834 | // #13: Index number / Value of referred-to fault-history buffer entry. 835 | void OpenThermGWClimate::process_Slave_MSG_FHB_INDEX_FHB_VALUE(uint32_t &response) { 836 | uint8_t fhb_entry_index_no = getUBUInt8(response); // Index number of following Fault Buffer entry 837 | uint8_t fhb_entry_value = getLBUInt8(response); // Value of above referenced Fault Buffer entry 838 | ESP_LOGD(TAG, "fhb_entry_index_no: %d, fhb_entry_value: %d", fhb_entry_index_no, fhb_entry_value); 839 | } 840 | 841 | 842 | 843 | // The remote-parameter transfer-enable flags indicate which remote parameters are supported by the slave. 844 | // The remote-parameter read/write flags indicate whether the master can only read the parameter from the 845 | // slave, or whether it can also modify the parameter and write it back to the slave. An Unknown Data-Id 846 | // response to a Read Remote-Parameter-Flags message indicates no support for remote-parameters 847 | // (equivalent to all transfer-enable flags equal to zero). In these flag bytes bit 0 corresponds to 848 | // remote-boilerparameter 1 and bit 7 to remote-boiler-parameter 8. 849 | 850 | /* Class 8 : Control of Special Applications */ 851 | 852 | // #7: Signal for cooling plant. 853 | // The cooling control signal is to be used for cooling applications. First the master should determine if the slave 854 | // supports cooling by reading the slave configuration. Then the master can use the cooling control signal and 855 | // the cooling-enable flag (status) to control the cooling plant. The status of the cooling plant can be read from 856 | // the slave cooling status bit. 857 | void OpenThermGWClimate::process_Master_MSG_COOLING_CONTROL(uint32_t &request) { 858 | float cooling_control_signal = getFloat(request); // Cooling control signal (%) 859 | ESP_LOGD(TAG, "cooling_control_signal: %f", cooling_control_signal); 860 | } 861 | 862 | // The boiler capacity level setting is to be used for boiler sequencer applications. The control setpoint should 863 | // be set to maximum, and then the capacity level setting throttled back to the required value. The default value 864 | // in the slave device should be 100% (ie no throttling back of the capacity). The master can read the maximum 865 | // boiler capacity and minimum modulation levels from the slave if it supports these. 866 | 867 | // #14: Maximum relative modulation level setting 868 | void OpenThermGWClimate::process_Master_MSG_MAX_REL_MOD_LEVEL_SETTING(uint32_t &request) { 869 | // Maximum relative boiler modulation level setting for sequencer and off-low & pump control applications. 870 | float max_rel_mod_level = getFloat(request); 871 | if (this->max_relative_modulation_level.has_value()) { 872 | max_rel_mod_level = this->max_relative_modulation_level.value(); 873 | request = modifyMsgData(request, max_rel_mod_level); 874 | } 875 | ESP_LOGD(TAG, "max_rel_mod_level: %f", max_rel_mod_level); 876 | } 877 | 878 | // #15: Maximum boiler capacity (kW) / Minimum boiler modulation level(%) 879 | void OpenThermGWClimate::process_Slave_MSG_MAX_CAPACITY_MIN_MOD_LEVEL(uint32_t &response) { 880 | uint8_t max_boiler_capacity = getUBUInt8(response); // 0..255kW 881 | uint8_t min_modulation_level = getLBUInt8(response); // 0..100% expressed as a percentage of the maximum capacity. 882 | ESP_LOGD(TAG, "max_boiler_capacity: %d, min_modulation_level: %d", max_boiler_capacity, min_modulation_level); 883 | } 884 | 885 | // There are applications where it’s necessary to override the room setpoint of the master (room-unit). 886 | // For instance in situations where room controls are connected to home or building controls or room controls in 887 | // holiday houses which are activated/controlled remotely. 888 | // 889 | // The master can read on Data ID 9 the remote override room setpoint. A value unequal to zero is a valid 890 | // remote override room setpoint. A value of zero means no remote override room setpoint. ID100 defines how 891 | // the master should react while remote room setpoint is active and there is a manual setpoint change and/or a 892 | // program setpoint change. 893 | 894 | // #9: Remote override room setpoint 895 | void OpenThermGWClimate::process_Slave_MSG_TROVERRIDE(uint32_t &response) { 896 | float remote_override_room_setpoint = getFloat(response); 897 | ESP_LOGD(TAG, "remote_override_room_setpoint: %f", remote_override_room_setpoint); 898 | } 899 | 900 | // #100: Remote override function 901 | void OpenThermGWClimate::process_Slave_MSG_REMOTE_OVERRIDE_FUNCTION(uint32_t &response) { 902 | uint8_t remote_override_function = getLBUInt8(response); 903 | ESP_LOGD(TAG, "remote_override_function: %d", remote_override_function); 904 | } 905 | 906 | 907 | } // namespace opentherm 908 | } // namespace esphome 909 | -------------------------------------------------------------------------------- /components/opentherm/opentherm_gw_climate.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/core/automation.h" 5 | #include "esphome/components/sensor/sensor.h" 6 | #include "esphome/components/binary_sensor/binary_sensor.h" 7 | #include "esphome/components/climate/climate.h" 8 | #include "esphome/components/climate/climate_mode.h" 9 | #include "esphome/components/climate/climate_traits.h" 10 | #include "opentherm.h" 11 | 12 | namespace esphome { 13 | namespace opentherm { 14 | 15 | class OpenThermGWClimate : public climate::Climate, public Component { 16 | public: 17 | OpenThermGWClimate(); 18 | void setup() override; 19 | void dump_config() override; 20 | void loop() override; 21 | 22 | protected: 23 | /// Override control to change settings of the climate device. 24 | void control(const climate::ClimateCall &call) override; 25 | /// Return the traits of this controller. 26 | climate::ClimateTraits traits() override; 27 | 28 | void processRequest(uint32_t request, OpenThermResponseStatus status); 29 | void processResponse(uint32_t &response, OpenThermResponseStatus status); 30 | 31 | void process_Master_MSG_COMMAND(uint32_t &request); 32 | void process_Master_MSG_DATE(uint32_t &request); 33 | void process_Master_MSG_DAY_TIME(uint32_t &request); 34 | void process_Master_MSG_MASTER_VERSION(uint32_t &request); 35 | void process_Master_MSG_M_CONFIG_M_MEMBERIDCODE(uint32_t &request); 36 | void process_Master_MSG_OPENTHERM_VERSION_MASTER(uint32_t &request); 37 | void process_Master_MSG_STATUS(uint32_t &request); 38 | void process_Master_MSG_TR(uint32_t &request); 39 | void process_Master_MSG_TRSET(uint32_t &request); 40 | void process_Master_MSG_TRSETCH2(uint32_t &request); 41 | void process_Master_MSG_TSET(uint32_t &request); 42 | void process_Master_MSG_TSETCH2(uint32_t &request); 43 | void process_Master_MSG_YEAR(uint32_t &request); 44 | void process_Master_MSG_TDHWSET(uint32_t &request); 45 | void process_Master_MSG_MAXTSET(uint32_t &request); 46 | void process_Master_MSG_TSP_INDEX_TSP_VALUE(uint32_t &response); 47 | void process_Master_MSG_COOLING_CONTROL(uint32_t &request); 48 | void process_Master_MSG_MAX_REL_MOD_LEVEL_SETTING(uint32_t &request); 49 | 50 | void process_Slave_MSG_ASF_FLAGS_OEM_FAULT_CODE(uint32_t &response); 51 | void process_Slave_MSG_BURNER_OPERATION_HOURS(uint32_t &response); 52 | void process_Slave_MSG_BURNER_STARTS(uint32_t &response); 53 | void process_Slave_MSG_CH_PRESSURE(uint32_t &response); 54 | void process_Slave_MSG_CH_PUMP_OPERATION_HOURS(uint32_t &response); 55 | void process_Slave_MSG_CH_PUMP_STARTS(uint32_t &response); 56 | void process_Slave_MSG_COMMAND(uint32_t &response); 57 | void process_Slave_MSG_DATE(uint32_t &response); 58 | void process_Slave_MSG_DAY_TIME(uint32_t &response); 59 | void process_Slave_MSG_DHW_BURNER_OPERATION_HOURS(uint32_t &response); 60 | void process_Slave_MSG_DHW_BURNER_STARTS(uint32_t &response); 61 | void process_Slave_MSG_DHW_FLOW_RATE(uint32_t &response); 62 | void process_Slave_MSG_DHW_PUMP_VALVE_OPERATION_HOURS(uint32_t &response); 63 | void process_Slave_MSG_DHW_PUMP_VALVE_STARTS(uint32_t &response); 64 | void process_Slave_MSG_OEM_DIAGNOSTIC_CODE(uint32_t &response); 65 | void process_Slave_MSG_OPENTHERM_VERSION_SLAVE(uint32_t &response); 66 | void process_Slave_MSG_REL_MOD_LEVEL(uint32_t &response); 67 | void process_Slave_MSG_REMOTE_OVERRIDE_FUNCTION(uint32_t &response); 68 | void process_Slave_MSG_SLAVE_VERSION(uint32_t &response); 69 | void process_Slave_MSG_STATUS(uint32_t &response); 70 | void process_Slave_MSG_S_CONFIG_S_MEMBERIDCODE(uint32_t &response); 71 | void process_Slave_MSG_TBOILER(uint32_t &response); 72 | void process_Slave_MSG_TCOLLECTOR(uint32_t &response); 73 | void process_Slave_MSG_TDHW(uint32_t &response); 74 | void process_Slave_MSG_TDHW2(uint32_t &response); 75 | void process_Slave_MSG_TEXHAUST(uint32_t &response); 76 | void process_Slave_MSG_TFLOWCH2(uint32_t &response); 77 | void process_Slave_MSG_TOUTSIDE(uint32_t &response); 78 | void process_Slave_MSG_TRET(uint32_t &response); 79 | void process_Slave_MSG_TROVERRIDE(uint32_t &response); 80 | void process_Slave_MSG_TSTORAGE(uint32_t &response); 81 | void process_Slave_MSG_YEAR(uint32_t &response); 82 | void process_Slave_MSG_RBP_FLAGS(uint32_t &response); 83 | void process_Slave_MSG_TDHWSET_UB_LB(uint32_t &response); 84 | void process_Slave_MSG_MAXTSET_UB_LB(uint32_t &response); 85 | void process_Slave_MSG_TDHWSET(uint32_t &request); 86 | void process_Slave_MSG_MAXTSET(uint32_t &request); 87 | void process_Slave_MSG_TSP(uint32_t &response); 88 | void process_Slave_MSG_TSP_INDEX_TSP_VALUE(uint32_t &response); 89 | void process_Slave_MSG_FHB_SIZE(uint32_t &response); 90 | void process_Slave_MSG_FHB_INDEX_FHB_VALUE(uint32_t &response); 91 | void process_Slave_MSG_MAX_CAPACITY_MIN_MOD_LEVEL(uint32_t &response); 92 | 93 | OpenThermChannel mOT; 94 | OpenThermChannel sOT; 95 | 96 | public: 97 | 98 | // If a maximum relative modulation level value has been configured, the gateway 99 | // will send the configured setpoint instead of the one received from the thermostat. 100 | optional max_relative_modulation_level; 101 | 102 | // If a hot water setpoint value has been configured, the gateway will send 103 | // the configured setpoint instead of the one received from the thermostat. 104 | optional dhw_setpoint; 105 | 106 | // If a max central heating setpoint value has been configured, the gateway will send 107 | // the configured setpoint instead of the one received from the thermostat. 108 | optional max_ch_water_setpoint; 109 | 110 | void set_thermostat_in_pin(InternalGPIOPin *thermostat_in_pin) { mOT.set_pin_in(thermostat_in_pin); } 111 | void set_thermostat_out_pin(InternalGPIOPin *thermostat_out_pin) { mOT.set_pin_out(thermostat_out_pin); } 112 | void set_boiler_in_pin(InternalGPIOPin *boiler_in_pin) { sOT.set_pin_in(boiler_in_pin); } 113 | void set_boiler_out_pin(InternalGPIOPin *boiler_out_pin) { sOT.set_pin_out(boiler_out_pin); } 114 | 115 | binary_sensor::BinarySensor *is_ch2_active{nullptr}; 116 | binary_sensor::BinarySensor *is_ch_active{nullptr}; 117 | binary_sensor::BinarySensor *is_cooling_active{nullptr}; 118 | binary_sensor::BinarySensor *is_dhw_active{nullptr}; 119 | binary_sensor::BinarySensor *is_diagnostic_event{nullptr}; 120 | binary_sensor::BinarySensor *is_fault_indication{nullptr}; 121 | binary_sensor::BinarySensor *is_flame_on{nullptr}; 122 | sensor::Sensor *boiler_water_temp{nullptr}; 123 | sensor::Sensor *burner_operation_hours{nullptr}; 124 | sensor::Sensor *burner_starts{nullptr}; 125 | sensor::Sensor *ch_pump_operation_hours{nullptr}; 126 | sensor::Sensor *ch_pump_starts{nullptr}; 127 | sensor::Sensor *ch_water_pressure{nullptr}; 128 | sensor::Sensor *dhw2_temperature{nullptr}; 129 | sensor::Sensor *dhw_burner_operation_hours{nullptr}; 130 | sensor::Sensor *dhw_burner_starts{nullptr}; 131 | sensor::Sensor *dhw_flow_rate{nullptr}; 132 | sensor::Sensor *dhw_pump_valve_operation_hours{nullptr}; 133 | sensor::Sensor *dhw_pump_valve_starts{nullptr}; 134 | sensor::Sensor *dhw_temperature{nullptr}; 135 | sensor::Sensor *exhaust_temperature{nullptr}; 136 | sensor::Sensor *flow_temperature_ch2{nullptr}; 137 | sensor::Sensor *outside_air_temperature{nullptr}; 138 | sensor::Sensor *relative_modulation_level{nullptr}; 139 | sensor::Sensor *return_water_temperature{nullptr}; 140 | sensor::Sensor *solar_collector_temperature{nullptr}; 141 | sensor::Sensor *solar_storage_temperature{nullptr}; 142 | 143 | void set_is_ch2_active(binary_sensor::BinarySensor *ch2_active) {this->is_ch2_active =ch2_active; }; 144 | void set_is_ch_active(binary_sensor::BinarySensor *ch_active) {this->is_ch_active =ch_active; }; 145 | void set_is_cooling_active(binary_sensor::BinarySensor *cooling_active) {this->is_cooling_active =cooling_active; }; 146 | void set_is_dhw_active(binary_sensor::BinarySensor *dhw_active) {this->is_dhw_active =dhw_active; }; 147 | void set_is_diagnostic_event(binary_sensor::BinarySensor *diagnostic_event) {this->is_diagnostic_event =diagnostic_event; }; 148 | void set_is_fault_indication(binary_sensor::BinarySensor *fault_indication) {this->is_fault_indication =fault_indication; }; 149 | void set_is_flame_on(binary_sensor::BinarySensor *flame_on) {this->is_flame_on =flame_on; }; 150 | void set_boiler_water_temp(sensor::Sensor *boiler_water_temp) {this->boiler_water_temp = boiler_water_temp;}; 151 | void set_burner_operation_hours(sensor::Sensor *burner_operation_hours) {this->burner_operation_hours = burner_operation_hours;}; 152 | void set_burner_starts(sensor::Sensor *burner_starts) {this->burner_starts = burner_starts;}; 153 | void set_ch_pump_operation_hours(sensor::Sensor *ch_pump_operation_hours) {this->ch_pump_operation_hours = ch_pump_operation_hours;}; 154 | void set_ch_pump_starts(sensor::Sensor *ch_pump_starts) {this->ch_pump_starts = ch_pump_starts;}; 155 | void set_ch_water_pressure(sensor::Sensor *ch_water_pressure) {this->ch_water_pressure = ch_water_pressure;}; 156 | void set_dhw2_temperature(sensor::Sensor *dhw2_temperature) {this->dhw2_temperature = dhw2_temperature;}; 157 | void set_dhw_burner_operation_hours(sensor::Sensor *dhw_burner_operation_hours) {this->dhw_burner_operation_hours = dhw_burner_operation_hours;}; 158 | void set_dhw_burner_starts(sensor::Sensor *dhw_burner_starts) {this->dhw_burner_starts = dhw_burner_starts;}; 159 | void set_dhw_flow_rate(sensor::Sensor *dhw_flow_rate) {this->dhw_flow_rate = dhw_flow_rate;}; 160 | void set_dhw_pump_valve_operation_hours(sensor::Sensor *dhw_pump_valve_operation_hours) {this->dhw_pump_valve_operation_hours = dhw_pump_valve_operation_hours;}; 161 | void set_dhw_pump_valve_starts(sensor::Sensor *dhw_pump_valve_starts) {this->dhw_pump_valve_starts = dhw_pump_valve_starts;}; 162 | void set_dhw_temperature(sensor::Sensor *dhw_temperature) {this->dhw_temperature = dhw_temperature;}; 163 | void set_exhaust_temperature(sensor::Sensor *exhaust_temperature) {this->exhaust_temperature = exhaust_temperature;}; 164 | void set_flow_temperature_ch2(sensor::Sensor *flow_temperature_ch2) {this->flow_temperature_ch2 = flow_temperature_ch2;}; 165 | void set_outside_air_temperature(sensor::Sensor *outside_air_temperature) {this->outside_air_temperature = outside_air_temperature;}; 166 | void set_relative_modulation_level(sensor::Sensor *relative_modulation_level) {this->relative_modulation_level = relative_modulation_level;}; 167 | void set_return_water_temperature(sensor::Sensor *return_water_temperature) {this->return_water_temperature = return_water_temperature;}; 168 | void set_solar_collector_temperature(sensor::Sensor *solar_collector_temperature) {this->solar_collector_temperature = solar_collector_temperature;}; 169 | void set_solar_storage_temperature(sensor::Sensor *solar_storage_temperature) {this->solar_storage_temperature = solar_storage_temperature;}; 170 | }; 171 | 172 | } // namespace opentherm 173 | } // namespace esphome 174 | -------------------------------------------------------------------------------- /doc/Opentherm Protocol v2-2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wichers/esphome-opentherm/5f03d0b7f2c1c2358740392636e9dd2bb3ff3c03/doc/Opentherm Protocol v2-2.pdf -------------------------------------------------------------------------------- /doc/messages.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wichers/esphome-opentherm/5f03d0b7f2c1c2358740392636e9dd2bb3ff3c03/doc/messages.csv -------------------------------------------------------------------------------- /opentherm.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: opentherm_gateway 3 | platform: ESP32 4 | board: esp-wrover-kit 5 | 6 | wifi: 7 | ssid: !secret wifi_ssid 8 | password: !secret wifi_password 9 | 10 | # Enable logging 11 | logger: 12 | baud_rate: 0 13 | level: DEBUG 14 | 15 | api: 16 | ota: 17 | web_server: 18 | port: 80 19 | 20 | sensor: 21 | binary_sensor: 22 | 23 | external_components: 24 | - source: 25 | type: git 26 | url: https://github.com/wichers/esphome-opentherm 27 | components: [openthermgateway] 28 | 29 | openthermgateway: 30 | name: "OpenTherm Gateway" 31 | 32 | thermostat_in_pin: 26 33 | thermostat_out_pin: 17 34 | boiler_in_pin: 25 35 | boiler_out_pin: 16 36 | 37 | fan_supply_air_percentage: 38 | name: "Fan supply (%)" 39 | fan_exhaust_air_percentage: 40 | name: "Fan exhaust (%)" 41 | fan_speed_supply: 42 | name: "Supply fan speed" 43 | fan_speed_exhaust: 44 | name: "Exhaust fan speed" 45 | is_ch_active: 46 | name: "Central heating active" 47 | is_dhw_active: 48 | name: "Domestic hot water active" 49 | is_flame_on: 50 | name: "Flame on" 51 | is_diagnostic_event: 52 | name: "Diagnostic event active" 53 | is_fault_indication: 54 | name: "Fault indication active" 55 | boiler_water_temp: 56 | name: "Boiler water temperature" 57 | dhw_temperature: 58 | name: "Domestic hot water temperature" 59 | return_water_temperature: 60 | name: "Return water temperature" 61 | relative_modulation_level: 62 | name: "Relative modulation level" 63 | -------------------------------------------------------------------------------- /opentherm_esp8266.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | includes: 3 | - opentherm.h 4 | - opentherm.cpp 5 | - opentherm_gw_climate.h 6 | - opentherm_gw_climate.cpp 7 | name: opentherm_gateway 8 | platform: ESP8266 9 | board: d1_mini 10 | 11 | wifi: 12 | ssid: !secret wifi_ssid 13 | password: !secret wifi_password 14 | 15 | 16 | # Enable logging 17 | logger: 18 | baud_rate: 0 19 | level: DEBUG 20 | 21 | api: 22 | ota: 23 | web_server: 24 | port: 80 25 | 26 | sensor: 27 | binary_sensor: 28 | 29 | climate: 30 | - platform: custom 31 | lambda: |- 32 | auto thermostat_in = new esphome::esp8266::ESP8266GPIOPin(); 33 | thermostat_in->set_pin(12); 34 | thermostat_in->set_flags(gpio::FLAG_INPUT); 35 | auto thermostat_out = new esphome::esp8266::ESP8266GPIOPin(); 36 | thermostat_out->set_pin(13); 37 | thermostat_out->set_flags(gpio::FLAG_OUTPUT); 38 | auto boiler_in = new esphome::esp8266::ESP8266GPIOPin(); 39 | boiler_in->set_pin(4); 40 | boiler_in->set_flags(gpio::FLAG_INPUT); 41 | auto boiler_out = new esphome::esp8266::ESP8266GPIOPin(); 42 | boiler_out->set_pin(5); 43 | boiler_out->set_flags(gpio::FLAG_OUTPUT); 44 | auto ot = new esphome::opentherm::OpenThermGWClimate(thermostat_in, thermostat_out, boiler_in, boiler_out); 45 | App.register_component(ot); 46 | ot->ch_active = new BinarySensor("CH Active"); 47 | App.register_binary_sensor(ot->ch_active); 48 | ot->dhw_active = new BinarySensor("DHW Active"); 49 | App.register_binary_sensor(ot->dhw_active); 50 | ot->flame_on = new BinarySensor("Flame On"); 51 | App.register_binary_sensor(ot->flame_on); 52 | ot->diagnostic_event = new BinarySensor("Diagnostic Event"); 53 | App.register_binary_sensor(ot->diagnostic_event); 54 | ot->fault_indication = new BinarySensor("Fault Indication"); 55 | App.register_binary_sensor(ot->fault_indication); 56 | ot->boiler_water_temp = new Sensor("Boiler Water Temperature"); 57 | App.register_sensor(ot->boiler_water_temp); 58 | ot->dhw_temperature = new Sensor("DHW Temperature"); 59 | App.register_sensor(ot->dhw_temperature); 60 | ot->return_water_temperature = new Sensor("Return Water Temperature"); 61 | App.register_sensor(ot->return_water_temperature); 62 | ot->relative_modulation_level = new Sensor("Relative Modulation Level"); 63 | App.register_climate(ot); 64 | return {ot}; 65 | climates: 66 | - name: "livingroom" -------------------------------------------------------------------------------- /simple_thermostat.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wichers/esphome-opentherm/5f03d0b7f2c1c2358740392636e9dd2bb3ff3c03/simple_thermostat.yaml --------------------------------------------------------------------------------