mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	[opentherm] Message ordering, on-the-fly message editing, code improvements (#7903)
This commit is contained in:
		| @@ -1,10 +1,12 @@ | |||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | from esphome import automation | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome import pins | from esphome import pins | ||||||
| from esphome.components import sensor | from esphome.components import sensor | ||||||
| from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 | from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266, CONF_TRIGGER_ID | ||||||
| from . import const, schema, validate, generate | from . import const, schema, validate, generate | ||||||
|  |  | ||||||
| CODEOWNERS = ["@olegtarasov"] | CODEOWNERS = ["@olegtarasov"] | ||||||
| @@ -20,7 +22,21 @@ CONF_CH2_ACTIVE = "ch2_active" | |||||||
| CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" | CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" | ||||||
| CONF_DHW_BLOCK = "dhw_block" | CONF_DHW_BLOCK = "dhw_block" | ||||||
| CONF_SYNC_MODE = "sync_mode" | CONF_SYNC_MODE = "sync_mode" | ||||||
| CONF_OPENTHERM_VERSION = "opentherm_version" | CONF_OPENTHERM_VERSION = "opentherm_version"  # Deprecated, will be removed | ||||||
|  | CONF_BEFORE_SEND = "before_send" | ||||||
|  | CONF_BEFORE_PROCESS_RESPONSE = "before_process_response" | ||||||
|  |  | ||||||
|  | # Triggers | ||||||
|  | BeforeSendTrigger = generate.opentherm_ns.class_( | ||||||
|  |     "BeforeSendTrigger", | ||||||
|  |     automation.Trigger.template(generate.OpenthermData.operator("ref")), | ||||||
|  | ) | ||||||
|  | BeforeProcessResponseTrigger = generate.opentherm_ns.class_( | ||||||
|  |     "BeforeProcessResponseTrigger", | ||||||
|  |     automation.Trigger.template(generate.OpenthermData.operator("ref")), | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
| @@ -36,7 +52,19 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, |             cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, | ||||||
|             cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, |             cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, | ||||||
|             cv.Optional(CONF_SYNC_MODE, False): cv.boolean, |             cv.Optional(CONF_SYNC_MODE, False): cv.boolean, | ||||||
|             cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, |             cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float,  # Deprecated | ||||||
|  |             cv.Optional(CONF_BEFORE_SEND): automation.validate_automation( | ||||||
|  |                 { | ||||||
|  |                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation( | ||||||
|  |                 { | ||||||
|  |                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||||
|  |                         BeforeProcessResponseTrigger | ||||||
|  |                     ), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend( |     .extend( | ||||||
| @@ -44,6 +72,11 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) |             schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) | ||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|  |     .extend( | ||||||
|  |         validate.create_entities_schema( | ||||||
|  |             schema.SETTINGS, (lambda s: s.validation_schema) | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|     .extend(cv.COMPONENT_SCHEMA), |     .extend(cv.COMPONENT_SCHEMA), | ||||||
|     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), |     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), | ||||||
| ) | ) | ||||||
| @@ -60,18 +93,33 @@ async def to_code(config: dict[str, Any]) -> None: | |||||||
|     out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN]) |     out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN]) | ||||||
|     cg.add(var.set_out_pin(out_pin)) |     cg.add(var.set_out_pin(out_pin)) | ||||||
|  |  | ||||||
|     non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} |     non_sensors = { | ||||||
|  |         CONF_ID, | ||||||
|  |         CONF_IN_PIN, | ||||||
|  |         CONF_OUT_PIN, | ||||||
|  |         CONF_BEFORE_SEND, | ||||||
|  |         CONF_BEFORE_PROCESS_RESPONSE, | ||||||
|  |     } | ||||||
|     input_sensors = [] |     input_sensors = [] | ||||||
|  |     settings = [] | ||||||
|     for key, value in config.items(): |     for key, value in config.items(): | ||||||
|         if key in non_sensors: |         if key in non_sensors: | ||||||
|             continue |             continue | ||||||
|         if key in schema.INPUTS: |         if key in schema.INPUTS: | ||||||
|             input_sensor = await cg.get_variable(value) |             input_sensor = await cg.get_variable(value) | ||||||
|             cg.add( |             cg.add(getattr(var, f"set_{key}_{const.INPUT_SENSOR}")(input_sensor)) | ||||||
|                 getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor) |  | ||||||
|             ) |  | ||||||
|             input_sensors.append(key) |             input_sensors.append(key) | ||||||
|  |         elif key in schema.SETTINGS: | ||||||
|  |             if value == schema.SETTINGS[key].default_value: | ||||||
|  |                 continue | ||||||
|  |             cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value)) | ||||||
|  |             settings.append(key) | ||||||
|         else: |         else: | ||||||
|  |             if key == CONF_OPENTHERM_VERSION: | ||||||
|  |                 _LOGGER.warning( | ||||||
|  |                     "opentherm_version is deprecated and will be removed in esphome 2025.2.0\n" | ||||||
|  |                     "Please change to 'opentherm_version_controller'." | ||||||
|  |                 ) | ||||||
|             cg.add(getattr(var, f"set_{key}")(value)) |             cg.add(getattr(var, f"set_{key}")(value)) | ||||||
|  |  | ||||||
|     if len(input_sensors) > 0: |     if len(input_sensors) > 0: | ||||||
| @@ -81,3 +129,21 @@ async def to_code(config: dict[str, Any]) -> None: | |||||||
|         ) |         ) | ||||||
|         generate.define_readers(const.INPUT_SENSOR, input_sensors) |         generate.define_readers(const.INPUT_SENSOR, input_sensors) | ||||||
|         generate.add_messages(var, input_sensors, schema.INPUTS) |         generate.add_messages(var, input_sensors, schema.INPUTS) | ||||||
|  |  | ||||||
|  |     if len(settings) > 0: | ||||||
|  |         generate.define_has_settings(settings, schema.SETTINGS) | ||||||
|  |         generate.define_message_handler(const.SETTING, settings, schema.SETTINGS) | ||||||
|  |         generate.define_setting_readers(const.SETTING, settings) | ||||||
|  |         generate.add_messages(var, settings, schema.SETTINGS) | ||||||
|  |  | ||||||
|  |     for conf in config.get(CONF_BEFORE_SEND, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation( | ||||||
|  |             trigger, [(generate.OpenthermData.operator("ref"), "x")], conf | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation( | ||||||
|  |             trigger, [(generate.OpenthermData.operator("ref"), "x")], conf | ||||||
|  |         ) | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								esphome/components/opentherm/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								esphome/components/opentherm/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "hub.h" | ||||||
|  | #include "opentherm.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | class BeforeSendTrigger : public Trigger<OpenthermData &> { | ||||||
|  |  public: | ||||||
|  |   BeforeSendTrigger(OpenthermHub *hub) { | ||||||
|  |     hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class BeforeProcessResponseTrigger : public Trigger<OpenthermData &> { | ||||||
|  |  public: | ||||||
|  |   BeforeProcessResponseTrigger(OpenthermHub *hub) { | ||||||
|  |     hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
| @@ -9,3 +9,4 @@ SWITCH = "switch" | |||||||
| NUMBER = "number" | NUMBER = "number" | ||||||
| OUTPUT = "output" | OUTPUT = "output" | ||||||
| INPUT_SENSOR = "input_sensor" | INPUT_SENSOR = "input_sensor" | ||||||
|  | SETTING = "setting" | ||||||
|   | |||||||
| @@ -1,13 +1,14 @@ | |||||||
| from collections.abc import Awaitable | from collections.abc import Awaitable | ||||||
| from typing import Any, Callable | from typing import Any, Callable, Optional | ||||||
|  |  | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.const import CONF_ID | from esphome.const import CONF_ID | ||||||
| from . import const | from . import const | ||||||
| from .schema import TSchema | from .schema import TSchema, SettingSchema | ||||||
|  |  | ||||||
| opentherm_ns = cg.esphome_ns.namespace("opentherm") | opentherm_ns = cg.esphome_ns.namespace("opentherm") | ||||||
| OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) | OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) | ||||||
|  | OpenthermData = opentherm_ns.class_("OpenthermData") | ||||||
|  |  | ||||||
|  |  | ||||||
| def define_has_component(component_type: str, keys: list[str]) -> None: | def define_has_component(component_type: str, keys: list[str]) -> None: | ||||||
| @@ -21,6 +22,24 @@ def define_has_component(component_type: str, keys: list[str]) -> None: | |||||||
|         cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}") |         cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # We need a separate set of macros for settings because there are different backing field types we need to take | ||||||
|  | # into account | ||||||
|  | def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> None: | ||||||
|  |     cg.add_define( | ||||||
|  |         "OPENTHERM_SETTING_LIST(F, sep)", | ||||||
|  |         cg.RawExpression( | ||||||
|  |             " sep ".join( | ||||||
|  |                 map( | ||||||
|  |                     lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})", | ||||||
|  |                     keys, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|  |     for key in keys: | ||||||
|  |         cg.add_define(f"OPENTHERM_HAS_SETTING_{key}") | ||||||
|  |  | ||||||
|  |  | ||||||
| def define_message_handler( | def define_message_handler( | ||||||
|     component_type: str, keys: list[str], schemas: dict[str, TSchema] |     component_type: str, keys: list[str], schemas: dict[str, TSchema] | ||||||
| ) -> None: | ) -> None: | ||||||
| @@ -74,16 +93,30 @@ def define_readers(component_type: str, keys: list[str]) -> None: | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): | def define_setting_readers(component_type: str, keys: list[str]) -> None: | ||||||
|     messages: set[tuple[str, bool]] = set() |  | ||||||
|     for key in keys: |     for key in keys: | ||||||
|         messages.add((schemas[key].message, schemas[key].keep_updated)) |         cg.add_define( | ||||||
|     for msg, keep_updated in messages: |             f"OPENTHERM_READ_{key}", | ||||||
|  |             cg.RawExpression(f"this->{key}_{component_type.lower()}"), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): | ||||||
|  |     messages: dict[str, tuple[bool, Optional[int]]] = {} | ||||||
|  |     for key in keys: | ||||||
|  |         messages[schemas[key].message] = ( | ||||||
|  |             schemas[key].keep_updated, | ||||||
|  |             schemas[key].order if hasattr(schemas[key], "order") else None, | ||||||
|  |         ) | ||||||
|  |     for msg, (keep_updated, order) in messages.items(): | ||||||
|         msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") |         msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") | ||||||
|         if keep_updated: |         if keep_updated: | ||||||
|             cg.add(hub.add_repeating_message(msg_expr)) |             cg.add(hub.add_repeating_message(msg_expr)) | ||||||
|         else: |         else: | ||||||
|             cg.add(hub.add_initial_message(msg_expr)) |             if order is not None: | ||||||
|  |                 cg.add(hub.add_initial_message(msg_expr, order)) | ||||||
|  |             else: | ||||||
|  |                 cg.add(hub.add_initial_message(msg_expr)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None: | def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None: | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ void write_f88(const float value, OpenthermData &data) { data.f88(value); } | |||||||
| OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | ||||||
|   OpenthermData data; |   OpenthermData data; | ||||||
|   data.type = 0; |   data.type = 0; | ||||||
|   data.id = 0; |   data.id = request_id; | ||||||
|   data.valueHB = 0; |   data.valueHB = 0; | ||||||
|   data.valueLB = 0; |   data.valueLB = 0; | ||||||
|  |  | ||||||
| @@ -82,28 +82,13 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | |||||||
|     // NOLINTEND |     // NOLINTEND | ||||||
|  |  | ||||||
|     data.type = MessageType::READ_DATA; |     data.type = MessageType::READ_DATA; | ||||||
|     data.id = MessageId::STATUS; |  | ||||||
|     data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) | |     data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) | | ||||||
|                    (summer_mode_is_active << 5) | (dhw_blocked << 6); |                    (summer_mode_is_active << 5) | (dhw_blocked << 6); | ||||||
|  |  | ||||||
|     return data; |     return data; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Another special case is OpenTherm version number which is configured at hub level as a constant |   // Next, we start with write requests from switches and other inputs, | ||||||
|   if (request_id == MessageId::OT_VERSION_CONTROLLER) { |  | ||||||
|     data.type = MessageType::WRITE_DATA; |  | ||||||
|     data.id = MessageId::OT_VERSION_CONTROLLER; |  | ||||||
|     data.f88(this->opentherm_version_); |  | ||||||
|  |  | ||||||
|     return data; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| // Disable incomplete switch statement warnings, because the cases in each |  | ||||||
| // switch are generated based on the configured sensors and inputs. |  | ||||||
| #pragma GCC diagnostic push |  | ||||||
| #pragma GCC diagnostic ignored "-Wswitch" |  | ||||||
|  |  | ||||||
|   // Next, we start with the write requests from switches and other inputs, |  | ||||||
|   // because we would want to write that data if it is available, rather than |   // because we would want to write that data if it is available, rather than | ||||||
|   // request a read for that type (in the case that both read and write are |   // request a read for that type (in the case that both read and write are | ||||||
|   // supported). |   // supported). | ||||||
| @@ -116,14 +101,23 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | |||||||
|                                       OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) |                                       OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||||
|     OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , |     OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , | ||||||
|                                             OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) |                                             OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||||
|  |     OPENTHERM_SETTING_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_SETTING, , | ||||||
|  |                                        OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Finally, handle the simple read requests, which only change with the message id. |   // Finally, handle the simple read requests, which only change with the message id. | ||||||
|   switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } |   switch (request_id) { | ||||||
|  |     OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|   switch (request_id) { |   switch (request_id) { | ||||||
|     OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) |     OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|   } |   } | ||||||
| #pragma GCC diagnostic pop |  | ||||||
|  |  | ||||||
|   // And if we get here, a message was requested which somehow wasn't handled. |   // And if we get here, a message was requested which somehow wasn't handled. | ||||||
|   // This shouldn't happen due to the way the defines are configured, so we |   // This shouldn't happen due to the way the defines are configured, so we | ||||||
| @@ -163,19 +157,37 @@ void OpenthermHub::setup() { | |||||||
|   // communicate at least once every second. Sending the status request is |   // communicate at least once every second. Sending the status request is | ||||||
|   // good practice anyway. |   // good practice anyway. | ||||||
|   this->add_repeating_message(MessageId::STATUS); |   this->add_repeating_message(MessageId::STATUS); | ||||||
|  |   this->write_initial_messages_(this->messages_); | ||||||
|   // Also ensure that we start communication with the STATUS message |   this->message_iterator_ = this->messages_.begin(); | ||||||
|   this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::STATUS); |  | ||||||
|  |  | ||||||
|   if (this->opentherm_version_ > 0.0f) { |  | ||||||
|     this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::OT_VERSION_CONTROLLER); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   this->current_message_iterator_ = this->initial_messages_.begin(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } | void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } | ||||||
|  |  | ||||||
|  | // Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?) | ||||||
|  | void OpenthermHub::write_initial_messages_(std::vector<MessageId> &target) {  // NOLINT | ||||||
|  |   std::vector<std::pair<MessageId, uint8_t>> sorted; | ||||||
|  |   std::copy_if(this->configured_messages_.begin(), this->configured_messages_.end(), std::back_inserter(sorted), | ||||||
|  |                [](const std::pair<MessageId, uint8_t> &pair) { return pair.second < REPEATING_MESSAGE_ORDER; }); | ||||||
|  |   std::sort(sorted.begin(), sorted.end(), | ||||||
|  |             [](const std::pair<MessageId, uint8_t> &a, const std::pair<MessageId, uint8_t> &b) { | ||||||
|  |               return a.second < b.second; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |   target.clear(); | ||||||
|  |   std::transform(sorted.begin(), sorted.end(), std::back_inserter(target), | ||||||
|  |                  [](const std::pair<MessageId, uint8_t> &pair) { return pair.first; }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?) | ||||||
|  | void OpenthermHub::write_repeating_messages_(std::vector<MessageId> &target) {  // NOLINT | ||||||
|  |   target.clear(); | ||||||
|  |   for (auto const &pair : this->configured_messages_) { | ||||||
|  |     if (pair.second == REPEATING_MESSAGE_ORDER) { | ||||||
|  |       target.push_back(pair.first); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| void OpenthermHub::loop() { | void OpenthermHub::loop() { | ||||||
|   if (this->sync_mode_) { |   if (this->sync_mode_) { | ||||||
|     this->sync_loop_(); |     this->sync_loop_(); | ||||||
| @@ -184,29 +196,18 @@ void OpenthermHub::loop() { | |||||||
|  |  | ||||||
|   auto cur_time = millis(); |   auto cur_time = millis(); | ||||||
|   auto const cur_mode = this->opentherm_->get_mode(); |   auto const cur_mode = this->opentherm_->get_mode(); | ||||||
|  |  | ||||||
|  |   if (this->handle_error_(cur_mode)) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   switch (cur_mode) { |   switch (cur_mode) { | ||||||
|     case OperationMode::WRITE: |     case OperationMode::WRITE: | ||||||
|     case OperationMode::READ: |     case OperationMode::READ: | ||||||
|     case OperationMode::LISTEN: |     case OperationMode::LISTEN: | ||||||
|       if (!this->check_timings_(cur_time)) { |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|       this->last_mode_ = cur_mode; |  | ||||||
|       break; |  | ||||||
|     case OperationMode::ERROR_PROTOCOL: |  | ||||||
|       if (this->last_mode_ == OperationMode::WRITE) { |  | ||||||
|         this->handle_protocol_write_error_(); |  | ||||||
|       } else if (this->last_mode_ == OperationMode::READ) { |  | ||||||
|         this->handle_protocol_read_error_(); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this->stop_opentherm_(); |  | ||||||
|       break; |  | ||||||
|     case OperationMode::ERROR_TIMEOUT: |  | ||||||
|       this->handle_timeout_error_(); |  | ||||||
|       this->stop_opentherm_(); |  | ||||||
|       break; |       break; | ||||||
|     case OperationMode::IDLE: |     case OperationMode::IDLE: | ||||||
|  |       this->check_timings_(cur_time); | ||||||
|       if (this->should_skip_loop_(cur_time)) { |       if (this->should_skip_loop_(cur_time)) { | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
| @@ -219,6 +220,28 @@ void OpenthermHub::loop() { | |||||||
|     case OperationMode::RECEIVED: |     case OperationMode::RECEIVED: | ||||||
|       this->read_response_(); |       this->read_response_(); | ||||||
|       break; |       break; | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |   this->last_mode_ = cur_mode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool OpenthermHub::handle_error_(OperationMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case OperationMode::ERROR_PROTOCOL: | ||||||
|  |       // Protocol error can happen only while reading boiler response. | ||||||
|  |       this->handle_protocol_error_(); | ||||||
|  |       return true; | ||||||
|  |     case OperationMode::ERROR_TIMEOUT: | ||||||
|  |       // Timeout error might happen while we wait for device to respond. | ||||||
|  |       this->handle_timeout_error_(); | ||||||
|  |       return true; | ||||||
|  |     case OperationMode::ERROR_TIMER: | ||||||
|  |       // Timer error can happen only on ESP32. | ||||||
|  |       this->handle_timer_error_(); | ||||||
|  |       return true; | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -237,16 +260,20 @@ void OpenthermHub::sync_loop_() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->start_conversation_(); |   this->start_conversation_(); | ||||||
|  |   // There may be a timer error at this point | ||||||
|  |   if (this->handle_error_(this->opentherm_->get_mode())) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Spin while message is being sent to device | ||||||
|   if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { |   if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { | ||||||
|     ESP_LOGE(TAG, "Hub timeout triggered during send"); |     ESP_LOGE(TAG, "Hub timeout triggered during send"); | ||||||
|     this->stop_opentherm_(); |     this->stop_opentherm_(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (this->opentherm_->is_error()) { |   // Check for errors and ensure we are in the right state (message sent successfully) | ||||||
|     this->handle_protocol_write_error_(); |   if (this->handle_error_(this->opentherm_->get_mode())) { | ||||||
|     this->stop_opentherm_(); |  | ||||||
|     return; |     return; | ||||||
|   } else if (!this->opentherm_->is_sent()) { |   } else if (!this->opentherm_->is_sent()) { | ||||||
|     ESP_LOGW(TAG, "Unexpected state after sending request: %s", |     ESP_LOGW(TAG, "Unexpected state after sending request: %s", | ||||||
| @@ -257,19 +284,20 @@ void OpenthermHub::sync_loop_() { | |||||||
|  |  | ||||||
|   // Listen for the response |   // Listen for the response | ||||||
|   this->opentherm_->listen(); |   this->opentherm_->listen(); | ||||||
|  |   // There may be a timer error at this point | ||||||
|  |   if (this->handle_error_(this->opentherm_->get_mode())) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Spin while response is being received | ||||||
|   if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { |   if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { | ||||||
|     ESP_LOGE(TAG, "Hub timeout triggered during receive"); |     ESP_LOGE(TAG, "Hub timeout triggered during receive"); | ||||||
|     this->stop_opentherm_(); |     this->stop_opentherm_(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (this->opentherm_->is_timeout()) { |   // Check for errors and ensure we are in the right state (message received successfully) | ||||||
|     this->handle_timeout_error_(); |   if (this->handle_error_(this->opentherm_->get_mode())) { | ||||||
|     this->stop_opentherm_(); |  | ||||||
|     return; |  | ||||||
|   } else if (this->opentherm_->is_protocol_error()) { |  | ||||||
|     this->handle_protocol_read_error_(); |  | ||||||
|     this->stop_opentherm_(); |  | ||||||
|     return; |     return; | ||||||
|   } else if (!this->opentherm_->has_message()) { |   } else if (!this->opentherm_->has_message()) { | ||||||
|     ESP_LOGW(TAG, "Unexpected state after receiving response: %s", |     ESP_LOGW(TAG, "Unexpected state after receiving response: %s", | ||||||
| @@ -281,17 +309,13 @@ void OpenthermHub::sync_loop_() { | |||||||
|   this->read_response_(); |   this->read_response_(); | ||||||
| } | } | ||||||
|  |  | ||||||
| bool OpenthermHub::check_timings_(uint32_t cur_time) { | void OpenthermHub::check_timings_(uint32_t cur_time) { | ||||||
|   if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) { |   if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) { | ||||||
|     ESP_LOGW(TAG, |     ESP_LOGW(TAG, | ||||||
|              "%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " |              "%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " | ||||||
|              "components that might slow the loop down.", |              "components that might slow the loop down.", | ||||||
|              (int) (cur_time - this->last_conversation_start_)); |              (int) (cur_time - this->last_conversation_start_)); | ||||||
|     this->stop_opentherm_(); |  | ||||||
|     return false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return true; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { | bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { | ||||||
| @@ -304,14 +328,17 @@ bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { | |||||||
| } | } | ||||||
|  |  | ||||||
| void OpenthermHub::start_conversation_() { | void OpenthermHub::start_conversation_() { | ||||||
|   if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { |   if (this->message_iterator_ == this->messages_.end()) { | ||||||
|     this->sending_initial_ = false; |     if (this->sending_initial_) { | ||||||
|     this->current_message_iterator_ = this->repeating_messages_.begin(); |       this->sending_initial_ = false; | ||||||
|   } else if (this->current_message_iterator_ == this->repeating_messages_.end()) { |       this->write_repeating_messages_(this->messages_); | ||||||
|     this->current_message_iterator_ = this->repeating_messages_.begin(); |     } | ||||||
|  |     this->message_iterator_ = this->messages_.begin(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto request = this->build_request_(*this->current_message_iterator_); |   auto request = this->build_request_(*this->message_iterator_); | ||||||
|  |  | ||||||
|  |   this->before_send_callback_.call(request); | ||||||
|  |  | ||||||
|   ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id, |   ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id, | ||||||
|            this->opentherm_->message_id_to_str((MessageId) request.id)); |            this->opentherm_->message_id_to_str((MessageId) request.id)); | ||||||
| @@ -331,37 +358,48 @@ void OpenthermHub::read_response_() { | |||||||
|  |  | ||||||
|   this->stop_opentherm_(); |   this->stop_opentherm_(); | ||||||
|  |  | ||||||
|  |   this->before_process_response_callback_.call(response); | ||||||
|   this->process_response(response); |   this->process_response(response); | ||||||
|  |  | ||||||
|   this->current_message_iterator_++; |   this->message_iterator_++; | ||||||
| } | } | ||||||
|  |  | ||||||
| void OpenthermHub::stop_opentherm_() { | void OpenthermHub::stop_opentherm_() { | ||||||
|   this->opentherm_->stop(); |   this->opentherm_->stop(); | ||||||
|   this->last_conversation_end_ = millis(); |   this->last_conversation_end_ = millis(); | ||||||
| } | } | ||||||
| void OpenthermHub::handle_protocol_write_error_() { |  | ||||||
|   ESP_LOGW(TAG, "Error while sending request: %s", | void OpenthermHub::handle_protocol_error_() { | ||||||
|            this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); |  | ||||||
|   this->opentherm_->debug_data(this->last_request_); |  | ||||||
| } |  | ||||||
| void OpenthermHub::handle_protocol_read_error_() { |  | ||||||
|   OpenThermError error; |   OpenThermError error; | ||||||
|   this->opentherm_->get_protocol_error(error); |   this->opentherm_->get_protocol_error(error); | ||||||
|   ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", |   ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", | ||||||
|            this->opentherm_->protocol_error_to_to_str(error.error_type)); |            this->opentherm_->protocol_error_to_str(error.error_type)); | ||||||
|   this->opentherm_->debug_error(error); |   this->opentherm_->debug_error(error); | ||||||
| } |  | ||||||
| void OpenthermHub::handle_timeout_error_() { |  | ||||||
|   ESP_LOGW(TAG, "Receive response timed out at a protocol level"); |  | ||||||
|   this->stop_opentherm_(); |   this->stop_opentherm_(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void OpenthermHub::handle_timeout_error_() { | ||||||
|  |   ESP_LOGW(TAG, "Timeout while waiting for response from device"); | ||||||
|  |   this->stop_opentherm_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void OpenthermHub::handle_timer_error_() { | ||||||
|  |   this->opentherm_->report_and_reset_timer_error(); | ||||||
|  |   this->stop_opentherm_(); | ||||||
|  |   // Timer error is critical, there is no point in retrying. | ||||||
|  |   this->mark_failed(); | ||||||
|  | } | ||||||
|  |  | ||||||
| void OpenthermHub::dump_config() { | void OpenthermHub::dump_config() { | ||||||
|  |   std::vector<MessageId> initial_messages; | ||||||
|  |   std::vector<MessageId> repeating_messages; | ||||||
|  |   this->write_initial_messages_(initial_messages); | ||||||
|  |   this->write_repeating_messages_(repeating_messages); | ||||||
|  |  | ||||||
|   ESP_LOGCONFIG(TAG, "OpenTherm:"); |   ESP_LOGCONFIG(TAG, "OpenTherm:"); | ||||||
|   LOG_PIN("  In: ", this->in_pin_); |   LOG_PIN("  In: ", this->in_pin_); | ||||||
|   LOG_PIN("  Out: ", this->out_pin_); |   LOG_PIN("  Out: ", this->out_pin_); | ||||||
|   ESP_LOGCONFIG(TAG, "  Sync mode: %d", this->sync_mode_); |   ESP_LOGCONFIG(TAG, "  Sync mode: %s", YESNO(this->sync_mode_)); | ||||||
|   ESP_LOGCONFIG(TAG, "  Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, ))); |   ESP_LOGCONFIG(TAG, "  Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, ))); | ||||||
|   ESP_LOGCONFIG(TAG, "  Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, ))); |   ESP_LOGCONFIG(TAG, "  Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, ))); | ||||||
|   ESP_LOGCONFIG(TAG, "  Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, ))); |   ESP_LOGCONFIG(TAG, "  Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, ))); | ||||||
| @@ -369,12 +407,12 @@ void OpenthermHub::dump_config() { | |||||||
|   ESP_LOGCONFIG(TAG, "  Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, ))); |   ESP_LOGCONFIG(TAG, "  Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, ))); | ||||||
|   ESP_LOGCONFIG(TAG, "  Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); |   ESP_LOGCONFIG(TAG, "  Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); | ||||||
|   ESP_LOGCONFIG(TAG, "  Initial requests:"); |   ESP_LOGCONFIG(TAG, "  Initial requests:"); | ||||||
|   for (auto type : this->initial_messages_) { |   for (auto type : initial_messages) { | ||||||
|     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str((type))); |     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str(type)); | ||||||
|   } |   } | ||||||
|   ESP_LOGCONFIG(TAG, "  Repeating requests:"); |   ESP_LOGCONFIG(TAG, "  Repeating requests:"); | ||||||
|   for (auto type : this->repeating_messages_) { |   for (auto type : repeating_messages) { | ||||||
|     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str((type))); |     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str(type)); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -38,6 +38,9 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace opentherm { | namespace opentherm { | ||||||
|  |  | ||||||
|  | static const uint8_t REPEATING_MESSAGE_ORDER = 255; | ||||||
|  | static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254; | ||||||
|  |  | ||||||
| // OpenTherm component for ESPHome | // OpenTherm component for ESPHome | ||||||
| class OpenthermHub : public Component { | class OpenthermHub : public Component { | ||||||
|  protected: |  protected: | ||||||
| @@ -58,15 +61,12 @@ class OpenthermHub : public Component { | |||||||
|  |  | ||||||
|   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) |   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) | ||||||
|  |  | ||||||
|   // The set of initial messages to send on starting communication with the boiler |   OPENTHERM_SETTING_LIST(OPENTHERM_DECLARE_SETTING, ) | ||||||
|   std::vector<MessageId> initial_messages_; |  | ||||||
|   // and the repeating messages which are sent repeatedly to update various sensors |  | ||||||
|   // and boiler parameters (like the setpoint). |  | ||||||
|   std::vector<MessageId> repeating_messages_; |  | ||||||
|   // Indicates if we are still working on the initial requests or not |  | ||||||
|   bool sending_initial_ = true; |   bool sending_initial_ = true; | ||||||
|   // Index for the current request in one of the _requests sets. |   std::unordered_map<MessageId, uint8_t> configured_messages_; | ||||||
|   std::vector<MessageId>::const_iterator current_message_iterator_; |   std::vector<MessageId> messages_; | ||||||
|  |   std::vector<MessageId>::const_iterator message_iterator_; | ||||||
|  |  | ||||||
|   uint32_t last_conversation_start_ = 0; |   uint32_t last_conversation_start_ = 0; | ||||||
|   uint32_t last_conversation_end_ = 0; |   uint32_t last_conversation_end_ = 0; | ||||||
| @@ -78,20 +78,25 @@ class OpenthermHub : public Component { | |||||||
|   // Very likely to happen while using Dallas temperature sensors. |   // Very likely to happen while using Dallas temperature sensors. | ||||||
|   bool sync_mode_ = false; |   bool sync_mode_ = false; | ||||||
|  |  | ||||||
|   float opentherm_version_ = 0.0f; |   CallbackManager<void(OpenthermData &)> before_send_callback_; | ||||||
|  |   CallbackManager<void(OpenthermData &)> before_process_response_callback_; | ||||||
|  |  | ||||||
|   // Create OpenTherm messages based on the message id |   // Create OpenTherm messages based on the message id | ||||||
|   OpenthermData build_request_(MessageId request_id) const; |   OpenthermData build_request_(MessageId request_id) const; | ||||||
|   void handle_protocol_write_error_(); |   bool handle_error_(OperationMode mode); | ||||||
|   void handle_protocol_read_error_(); |   void handle_protocol_error_(); | ||||||
|   void handle_timeout_error_(); |   void handle_timeout_error_(); | ||||||
|  |   void handle_timer_error_(); | ||||||
|   void stop_opentherm_(); |   void stop_opentherm_(); | ||||||
|   void start_conversation_(); |   void start_conversation_(); | ||||||
|   void read_response_(); |   void read_response_(); | ||||||
|   bool check_timings_(uint32_t cur_time); |   void check_timings_(uint32_t cur_time); | ||||||
|   bool should_skip_loop_(uint32_t cur_time) const; |   bool should_skip_loop_(uint32_t cur_time) const; | ||||||
|   void sync_loop_(); |   void sync_loop_(); | ||||||
|  |  | ||||||
|  |   void write_initial_messages_(std::vector<MessageId> &target); | ||||||
|  |   void write_repeating_messages_(std::vector<MessageId> &target); | ||||||
|  |  | ||||||
|   template<typename F> bool spin_wait_(uint32_t timeout, F func) { |   template<typename F> bool spin_wait_(uint32_t timeout, F func) { | ||||||
|     auto start_time = millis(); |     auto start_time = millis(); | ||||||
|     while (func()) { |     while (func()) { | ||||||
| @@ -127,13 +132,18 @@ class OpenthermHub : public Component { | |||||||
|  |  | ||||||
|   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) |   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_SETTING_LIST(OPENTHERM_SET_SETTING, ) | ||||||
|  |  | ||||||
|   // Add a request to the vector of initial requests |   // Add a request to the vector of initial requests | ||||||
|   void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); } |   void add_initial_message(MessageId message_id) { | ||||||
|  |     this->configured_messages_[message_id] = INITIAL_UNORDERED_MESSAGE_ORDER; | ||||||
|  |   } | ||||||
|  |   void add_initial_message(MessageId message_id, uint8_t order) { this->configured_messages_[message_id] = order; } | ||||||
|   // Add a request to the set of repeating requests. Note that a large number of repeating |   // Add a request to the set of repeating requests. Note that a large number of repeating | ||||||
|   // requests will slow down communication with the boiler. Each request may take up to 1 second, |   // requests will slow down communication with the boiler. Each request may take up to 1 second, | ||||||
|   // so with all sensors enabled, it may take about half a minute before a change in setpoint |   // so with all sensors enabled, it may take about half a minute before a change in setpoint | ||||||
|   // will be processed. |   // will be processed. | ||||||
|   void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); } |   void add_repeating_message(MessageId message_id) { this->configured_messages_[message_id] = REPEATING_MESSAGE_ORDER; } | ||||||
|  |  | ||||||
|   // There are seven status variables, which can either be set as a simple variable, |   // There are seven status variables, which can either be set as a simple variable, | ||||||
|   // or using a switch. ch_enable and dhw_enable default to true, the others to false. |   // or using a switch. ch_enable and dhw_enable default to true, the others to false. | ||||||
| @@ -149,7 +159,13 @@ class OpenthermHub : public Component { | |||||||
|   void set_summer_mode_active(bool value) { this->summer_mode_active = value; } |   void set_summer_mode_active(bool value) { this->summer_mode_active = value; } | ||||||
|   void set_dhw_block(bool value) { this->dhw_block = value; } |   void set_dhw_block(bool value) { this->dhw_block = value; } | ||||||
|   void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } |   void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } | ||||||
|   void set_opentherm_version(float value) { this->opentherm_version_ = value; } |  | ||||||
|  |   void add_on_before_send_callback(std::function<void(OpenthermData &)> &&callback) { | ||||||
|  |     this->before_send_callback_.add(std::move(callback)); | ||||||
|  |   } | ||||||
|  |   void add_on_before_process_response_callback(std::function<void(OpenthermData &)> &&callback) { | ||||||
|  |     this->before_process_response_callback_.add(std::move(callback)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } |   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -52,7 +52,9 @@ bool OpenTherm::initialize() { | |||||||
|   OpenTherm::instance = this; |   OpenTherm::instance = this; | ||||||
| #endif | #endif | ||||||
|   this->in_pin_->pin_mode(gpio::FLAG_INPUT); |   this->in_pin_->pin_mode(gpio::FLAG_INPUT); | ||||||
|  |   this->in_pin_->setup(); | ||||||
|   this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); |   this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||||
|  |   this->out_pin_->setup(); | ||||||
|   this->out_pin_->digital_write(true); |   this->out_pin_->digital_write(true); | ||||||
|  |  | ||||||
| #if defined(ESP32) || defined(USE_ESP_IDF) | #if defined(ESP32) || defined(USE_ESP_IDF) | ||||||
| @@ -182,7 +184,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) { | |||||||
|       } |       } | ||||||
|       arg->capture_ = 1;  // reset counter |       arg->capture_ = 1;  // reset counter | ||||||
|     } else if (arg->capture_ > 0xFF) { |     } else if (arg->capture_ > 0xFF) { | ||||||
|       // no change for too long, invalid mancheter encoding |       // no change for too long, invalid manchester encoding | ||||||
|       arg->mode_ = OperationMode::ERROR_PROTOCOL; |       arg->mode_ = OperationMode::ERROR_PROTOCOL; | ||||||
|       arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; |       arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; | ||||||
|       arg->stop_timer_(); |       arg->stop_timer_(); | ||||||
| @@ -312,21 +314,31 @@ bool OpenTherm::init_esp32_timer_() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { | void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { | ||||||
|   esp_err_t result; |   // We will report timer errors outside of interrupt handler | ||||||
|  |   this->timer_error_ = ESP_OK; | ||||||
|  |   this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; | ||||||
|  |  | ||||||
|   result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); |   this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); | ||||||
|   if (result != ESP_OK) { |   if (this->timer_error_ != ESP_OK) { | ||||||
|     const auto *error = esp_err_to_name(result); |     this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR; | ||||||
|     ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error); |     return; | ||||||
|  |   } | ||||||
|  |   this->timer_error_ = timer_start(this->timer_group_, this->timer_idx_); | ||||||
|  |   if (this->timer_error_ != ESP_OK) { | ||||||
|  |     this->timer_error_type_ = TimerErrorType::TIMER_START_ERROR; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void OpenTherm::report_and_reset_timer_error() { | ||||||
|  |   if (this->timer_error_ == ESP_OK) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   result = timer_start(this->timer_group_, this->timer_idx_); |   ESP_LOGE(TAG, "Error occured while manipulating timer (%s): %s", this->timer_error_to_str(this->timer_error_type_), | ||||||
|   if (result != ESP_OK) { |            esp_err_to_name(this->timer_error_)); | ||||||
|     const auto *error = esp_err_to_name(result); |  | ||||||
|     ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); |   this->timer_error_ = ESP_OK; | ||||||
|     return; |   this->timer_error_type_ = NO_TIMER_ERROR; | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // 5 kHz timer_ | // 5 kHz timer_ | ||||||
| @@ -343,21 +355,18 @@ void IRAM_ATTR OpenTherm::start_write_timer_() { | |||||||
|  |  | ||||||
| void IRAM_ATTR OpenTherm::stop_timer_() { | void IRAM_ATTR OpenTherm::stop_timer_() { | ||||||
|   InterruptLock const lock; |   InterruptLock const lock; | ||||||
|  |   // We will report timer errors outside of interrupt handler | ||||||
|  |   this->timer_error_ = ESP_OK; | ||||||
|  |   this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; | ||||||
|  |  | ||||||
|   esp_err_t result; |   this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_); | ||||||
|  |   if (this->timer_error_ != ESP_OK) { | ||||||
|   result = timer_pause(this->timer_group_, this->timer_idx_); |     this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR; | ||||||
|   if (result != ESP_OK) { |  | ||||||
|     const auto *error = esp_err_to_name(result); |  | ||||||
|     ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error); |  | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |   this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); | ||||||
|   result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); |   if (this->timer_error_ != ESP_OK) { | ||||||
|   if (result != ESP_OK) { |     this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR; | ||||||
|     const auto *error = esp_err_to_name(result); |  | ||||||
|     ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error); |  | ||||||
|     return; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -386,6 +395,9 @@ void IRAM_ATTR OpenTherm::stop_timer_() { | |||||||
|   timer1_detachInterrupt(); |   timer1_detachInterrupt(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // There is nothing to report on ESP8266 | ||||||
|  | void OpenTherm::report_and_reset_timer_error() {} | ||||||
|  |  | ||||||
| #endif  // END ESP8266 | #endif  // END ESP8266 | ||||||
|  |  | ||||||
| // https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd | // https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd | ||||||
| @@ -412,11 +424,12 @@ const char *OpenTherm::operation_mode_to_str(OperationMode mode) { | |||||||
|     TO_STRING_MEMBER(SENT) |     TO_STRING_MEMBER(SENT) | ||||||
|     TO_STRING_MEMBER(ERROR_PROTOCOL) |     TO_STRING_MEMBER(ERROR_PROTOCOL) | ||||||
|     TO_STRING_MEMBER(ERROR_TIMEOUT) |     TO_STRING_MEMBER(ERROR_TIMEOUT) | ||||||
|  |     TO_STRING_MEMBER(ERROR_TIMER) | ||||||
|     default: |     default: | ||||||
|       return "<INVALID>"; |       return "<INVALID>"; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { | const char *OpenTherm::protocol_error_to_str(ProtocolErrorType error_type) { | ||||||
|   switch (error_type) { |   switch (error_type) { | ||||||
|     TO_STRING_MEMBER(NO_ERROR) |     TO_STRING_MEMBER(NO_ERROR) | ||||||
|     TO_STRING_MEMBER(NO_TRANSITION) |     TO_STRING_MEMBER(NO_TRANSITION) | ||||||
| @@ -427,6 +440,17 @@ const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { | |||||||
|       return "<INVALID>"; |       return "<INVALID>"; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | const char *OpenTherm::timer_error_to_str(TimerErrorType error_type) { | ||||||
|  |   switch (error_type) { | ||||||
|  |     TO_STRING_MEMBER(NO_TIMER_ERROR) | ||||||
|  |     TO_STRING_MEMBER(SET_ALARM_VALUE_ERROR) | ||||||
|  |     TO_STRING_MEMBER(TIMER_START_ERROR) | ||||||
|  |     TO_STRING_MEMBER(TIMER_PAUSE_ERROR) | ||||||
|  |     TO_STRING_MEMBER(SET_COUNTER_VALUE_ERROR) | ||||||
|  |     default: | ||||||
|  |       return "<INVALID>"; | ||||||
|  |   } | ||||||
|  | } | ||||||
| const char *OpenTherm::message_type_to_str(MessageType message_type) { | const char *OpenTherm::message_type_to_str(MessageType message_type) { | ||||||
|   switch (message_type) { |   switch (message_type) { | ||||||
|     TO_STRING_MEMBER(READ_DATA) |     TO_STRING_MEMBER(READ_DATA) | ||||||
|   | |||||||
| @@ -36,11 +36,12 @@ enum OperationMode { | |||||||
|   READ = 2,      // reading 32-bit data frame |   READ = 2,      // reading 32-bit data frame | ||||||
|   RECEIVED = 3,  // data frame received with valid start and stop bit |   RECEIVED = 3,  // data frame received with valid start and stop bit | ||||||
|  |  | ||||||
|   WRITE = 4,  // writing data with timer_ |   WRITE = 4,  // writing data to output | ||||||
|   SENT = 5,   // all data written to output |   SENT = 5,   // all data written to output | ||||||
|  |  | ||||||
|   ERROR_PROTOCOL = 8,  // manchester protocol data transfer error |   ERROR_PROTOCOL = 8,  // protocol error, can happed only during READ | ||||||
|   ERROR_TIMEOUT = 9    // read timeout |   ERROR_TIMEOUT = 9,   // timeout while waiting for response from device, only during LISTEN | ||||||
|  |   ERROR_TIMER = 10     // error operating the ESP32 timer | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum ProtocolErrorType { | enum ProtocolErrorType { | ||||||
| @@ -51,6 +52,14 @@ enum ProtocolErrorType { | |||||||
|   NO_CHANGE_TOO_LONG = 4,  // No level change for too much timer ticks |   NO_CHANGE_TOO_LONG = 4,  // No level change for too much timer ticks | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | enum TimerErrorType { | ||||||
|  |   NO_TIMER_ERROR = 0,           // No error | ||||||
|  |   SET_ALARM_VALUE_ERROR = 1,    // No transition in the middle of the bit | ||||||
|  |   TIMER_START_ERROR = 2,        // Stop bit wasn't present when expected | ||||||
|  |   TIMER_PAUSE_ERROR = 3,        // Parity check didn't pass | ||||||
|  |   SET_COUNTER_VALUE_ERROR = 4,  // No level change for too much timer ticks | ||||||
|  | }; | ||||||
|  |  | ||||||
| enum MessageType { | enum MessageType { | ||||||
|   READ_DATA = 0, |   READ_DATA = 0, | ||||||
|   READ_ACK = 4, |   READ_ACK = 4, | ||||||
| @@ -299,7 +308,9 @@ class OpenTherm { | |||||||
|    * |    * | ||||||
|    * @return true if last listen() or send() operation ends up with an error. |    * @return true if last listen() or send() operation ends up with an error. | ||||||
|    */ |    */ | ||||||
|   bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; } |   bool is_error() { | ||||||
|  |     return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL || mode_ == ERROR_TIMER; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Indicates whether last listen() or send() operation ends up with a *timeout* error |    * Indicates whether last listen() or send() operation ends up with a *timeout* error | ||||||
| @@ -313,14 +324,22 @@ class OpenTherm { | |||||||
|    */ |    */ | ||||||
|   bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; } |   bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Indicates whether start_esp32_timer_() or stop_timer_() had an error. Only relevant when used on ESP32. | ||||||
|  |    * @return true if there was an error. | ||||||
|  |    */ | ||||||
|  |   bool is_timer_error() { return mode_ == OperationMode::ERROR_TIMER; } | ||||||
|  |  | ||||||
|   bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; } |   bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; } | ||||||
|  |  | ||||||
|   OperationMode get_mode() { return mode_; } |   OperationMode get_mode() { return mode_; } | ||||||
|  |  | ||||||
|   void debug_data(OpenthermData &data); |   void debug_data(OpenthermData &data); | ||||||
|   void debug_error(OpenThermError &error) const; |   void debug_error(OpenThermError &error) const; | ||||||
|  |   void report_and_reset_timer_error(); | ||||||
|  |  | ||||||
|   const char *protocol_error_to_to_str(ProtocolErrorType error_type); |   const char *protocol_error_to_str(ProtocolErrorType error_type); | ||||||
|  |   const char *timer_error_to_str(TimerErrorType error_type); | ||||||
|   const char *message_type_to_str(MessageType message_type); |   const char *message_type_to_str(MessageType message_type); | ||||||
|   const char *operation_mode_to_str(OperationMode mode); |   const char *operation_mode_to_str(OperationMode mode); | ||||||
|   const char *message_id_to_str(MessageId id); |   const char *message_id_to_str(MessageId id); | ||||||
| @@ -349,10 +368,12 @@ class OpenTherm { | |||||||
|   uint32_t data_; |   uint32_t data_; | ||||||
|   uint8_t bit_pos_; |   uint8_t bit_pos_; | ||||||
|   int32_t timeout_counter_;  // <0 no timeout |   int32_t timeout_counter_;  // <0 no timeout | ||||||
|  |  | ||||||
|   int32_t device_timeout_; |   int32_t device_timeout_; | ||||||
|  |  | ||||||
| #if defined(ESP32) || defined(USE_ESP_IDF) | #if defined(ESP32) || defined(USE_ESP_IDF) | ||||||
|  |   esp_err_t timer_error_ = ESP_OK; | ||||||
|  |   TimerErrorType timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; | ||||||
|  |  | ||||||
|   bool init_esp32_timer_(); |   bool init_esp32_timer_(); | ||||||
|   void start_esp32_timer_(uint64_t alarm_value); |   void start_esp32_timer_(uint64_t alarm_value); | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -28,6 +28,9 @@ namespace opentherm { | |||||||
| #ifndef OPENTHERM_INPUT_SENSOR_LIST | #ifndef OPENTHERM_INPUT_SENSOR_LIST | ||||||
| #define OPENTHERM_INPUT_SENSOR_LIST(F, sep) | #define OPENTHERM_INPUT_SENSOR_LIST(F, sep) | ||||||
| #endif | #endif | ||||||
|  | #ifndef OPENTHERM_SETTING_LIST | ||||||
|  | #define OPENTHERM_SETTING_LIST(F, sep) | ||||||
|  | #endif | ||||||
|  |  | ||||||
| // Use macros to create fields for every entity specified in the ESPHome configuration | // Use macros to create fields for every entity specified in the ESPHome configuration | ||||||
| #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; | #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; | ||||||
| @@ -36,6 +39,7 @@ namespace opentherm { | |||||||
| #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; | #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; | ||||||
| #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; | #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; | ||||||
| #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; | #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; | ||||||
|  | #define OPENTHERM_DECLARE_SETTING(type, entity, def) type entity = def; | ||||||
|  |  | ||||||
| // Setter macros | // Setter macros | ||||||
| #define OPENTHERM_SET_SENSOR(entity) \ | #define OPENTHERM_SET_SENSOR(entity) \ | ||||||
| @@ -56,6 +60,9 @@ namespace opentherm { | |||||||
| #define OPENTHERM_SET_INPUT_SENSOR(entity) \ | #define OPENTHERM_SET_INPUT_SENSOR(entity) \ | ||||||
|   void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } |   void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } | ||||||
|  |  | ||||||
|  | #define OPENTHERM_SET_SETTING(type, entity, def) \ | ||||||
|  |   void set_##entity(type value) { this->entity = value; } | ||||||
|  |  | ||||||
| // ===== hub.cpp macros ===== | // ===== hub.cpp macros ===== | ||||||
|  |  | ||||||
| // *_MESSAGE_HANDLERS are generated in defines.h and look like this: | // *_MESSAGE_HANDLERS are generated in defines.h and look like this: | ||||||
| @@ -85,6 +92,9 @@ namespace opentherm { | |||||||
| #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS | #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS | ||||||
| #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
| #endif | #endif | ||||||
|  | #ifndef OPENTHERM_SETTING_MESSAGE_HANDLERS | ||||||
|  | #define OPENTHERM_SETTING_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
|  | #endif | ||||||
|  |  | ||||||
| // Write data request builders | // Write data request builders | ||||||
| #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ | #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ | ||||||
| @@ -92,6 +102,7 @@ namespace opentherm { | |||||||
|     data.type = MessageType::WRITE_DATA; \ |     data.type = MessageType::WRITE_DATA; \ | ||||||
|     data.id = request_id; |     data.id = request_id; | ||||||
| #define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data); | #define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data); | ||||||
|  | #define OPENTHERM_MESSAGE_WRITE_SETTING(key, msg_data) message_data::write_##msg_data(this->key, data); | ||||||
| #define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \ | #define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \ | ||||||
|   return data; \ |   return data; \ | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -2,8 +2,9 @@ | |||||||
| # inputs of the OpenTherm component. | # inputs of the OpenTherm component. | ||||||
|  |  | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from typing import Optional, TypeVar | from typing import Optional, TypeVar, Any | ||||||
|  |  | ||||||
|  | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     UNIT_CELSIUS, |     UNIT_CELSIUS, | ||||||
|     UNIT_EMPTY, |     UNIT_EMPTY, | ||||||
| @@ -64,6 +65,7 @@ class SensorSchema(EntitySchema): | |||||||
|     icon: Optional[str] = None |     icon: Optional[str] = None | ||||||
|     device_class: Optional[str] = None |     device_class: Optional[str] = None | ||||||
|     disabled_by_default: bool = False |     disabled_by_default: bool = False | ||||||
|  |     order: Optional[int] = None | ||||||
|  |  | ||||||
|  |  | ||||||
| SENSORS: dict[str, SensorSchema] = { | SENSORS: dict[str, SensorSchema] = { | ||||||
| @@ -399,6 +401,7 @@ SENSORS: dict[str, SensorSchema] = { | |||||||
|         message="OT_VERSION_DEVICE", |         message="OT_VERSION_DEVICE", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="f88", |         message_data="f88", | ||||||
|  |         order=2, | ||||||
|     ), |     ), | ||||||
|     "device_type": SensorSchema( |     "device_type": SensorSchema( | ||||||
|         description="Device product type", |         description="Device product type", | ||||||
| @@ -409,6 +412,7 @@ SENSORS: dict[str, SensorSchema] = { | |||||||
|         message="VERSION_DEVICE", |         message="VERSION_DEVICE", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="u8_hb", |         message_data="u8_hb", | ||||||
|  |         order=0, | ||||||
|     ), |     ), | ||||||
|     "device_version": SensorSchema( |     "device_version": SensorSchema( | ||||||
|         description="Device product version", |         description="Device product version", | ||||||
| @@ -419,6 +423,7 @@ SENSORS: dict[str, SensorSchema] = { | |||||||
|         message="VERSION_DEVICE", |         message="VERSION_DEVICE", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="u8_lb", |         message_data="u8_lb", | ||||||
|  |         order=0, | ||||||
|     ), |     ), | ||||||
|     "device_id": SensorSchema( |     "device_id": SensorSchema( | ||||||
|         description="Device ID code", |         description="Device ID code", | ||||||
| @@ -429,6 +434,7 @@ SENSORS: dict[str, SensorSchema] = { | |||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="u8_lb", |         message_data="u8_lb", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "otc_hc_ratio_ub": SensorSchema( |     "otc_hc_ratio_ub": SensorSchema( | ||||||
|         description="OTC heat curve ratio upper bound", |         description="OTC heat curve ratio upper bound", | ||||||
| @@ -457,6 +463,7 @@ SENSORS: dict[str, SensorSchema] = { | |||||||
| class BinarySensorSchema(EntitySchema): | class BinarySensorSchema(EntitySchema): | ||||||
|     icon: Optional[str] = None |     icon: Optional[str] = None | ||||||
|     device_class: Optional[str] = None |     device_class: Optional[str] = None | ||||||
|  |     order: Optional[int] = None | ||||||
|  |  | ||||||
|  |  | ||||||
| BINARY_SENSORS: dict[str, BinarySensorSchema] = { | BINARY_SENSORS: dict[str, BinarySensorSchema] = { | ||||||
| @@ -525,48 +532,56 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = { | |||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_0", |         message_data="flag8_hb_0", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "control_type_on_off": BinarySensorSchema( |     "control_type_on_off": BinarySensorSchema( | ||||||
|         description="Configuration: Control type is on/off", |         description="Configuration: Control type is on/off", | ||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_1", |         message_data="flag8_hb_1", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "cooling_supported": BinarySensorSchema( |     "cooling_supported": BinarySensorSchema( | ||||||
|         description="Configuration: Cooling supported", |         description="Configuration: Cooling supported", | ||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_2", |         message_data="flag8_hb_2", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "dhw_storage_tank": BinarySensorSchema( |     "dhw_storage_tank": BinarySensorSchema( | ||||||
|         description="Configuration: DHW storage tank", |         description="Configuration: DHW storage tank", | ||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_3", |         message_data="flag8_hb_3", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "controller_pump_control_allowed": BinarySensorSchema( |     "controller_pump_control_allowed": BinarySensorSchema( | ||||||
|         description="Configuration: Controller pump control allowed", |         description="Configuration: Controller pump control allowed", | ||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_4", |         message_data="flag8_hb_4", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "ch2_present": BinarySensorSchema( |     "ch2_present": BinarySensorSchema( | ||||||
|         description="Configuration: CH2 present", |         description="Configuration: CH2 present", | ||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_5", |         message_data="flag8_hb_5", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "water_filling": BinarySensorSchema( |     "water_filling": BinarySensorSchema( | ||||||
|         description="Configuration: Remote water filling", |         description="Configuration: Remote water filling", | ||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_6", |         message_data="flag8_hb_6", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "heat_mode": BinarySensorSchema( |     "heat_mode": BinarySensorSchema( | ||||||
|         description="Configuration: Heating or cooling", |         description="Configuration: Heating or cooling", | ||||||
|         message="DEVICE_CONFIG", |         message="DEVICE_CONFIG", | ||||||
|         keep_updated=False, |         keep_updated=False, | ||||||
|         message_data="flag8_hb_7", |         message_data="flag8_hb_7", | ||||||
|  |         order=4, | ||||||
|     ), |     ), | ||||||
|     "dhw_setpoint_transfer_enabled": BinarySensorSchema( |     "dhw_setpoint_transfer_enabled": BinarySensorSchema( | ||||||
|         description="Remote boiler parameters: DHW setpoint transfer enabled", |         description="Remote boiler parameters: DHW setpoint transfer enabled", | ||||||
| @@ -812,3 +827,65 @@ INPUTS: dict[str, InputSchema] = { | |||||||
|         auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"), |         auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"), | ||||||
|     ), |     ), | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class SettingSchema(EntitySchema): | ||||||
|  |     backing_type: str | ||||||
|  |     validation_schema: cv.Schema | ||||||
|  |     default_value: Any | ||||||
|  |     order: Optional[int] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SETTINGS: dict[str, SettingSchema] = { | ||||||
|  |     "controller_product_type": SettingSchema( | ||||||
|  |         description="Controller product type", | ||||||
|  |         message="VERSION_CONTROLLER", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="u8_hb", | ||||||
|  |         backing_type="uint8_t", | ||||||
|  |         validation_schema=cv.int_range(min=0, max=255), | ||||||
|  |         default_value=0, | ||||||
|  |         order=1, | ||||||
|  |     ), | ||||||
|  |     "controller_product_version": SettingSchema( | ||||||
|  |         description="Controller product version", | ||||||
|  |         message="VERSION_CONTROLLER", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="u8_lb", | ||||||
|  |         backing_type="uint8_t", | ||||||
|  |         validation_schema=cv.int_range(min=0, max=255), | ||||||
|  |         default_value=0, | ||||||
|  |         order=1, | ||||||
|  |     ), | ||||||
|  |     "opentherm_version_controller": SettingSchema( | ||||||
|  |         description="Version of OpenTherm implemented by controller", | ||||||
|  |         message="OT_VERSION_CONTROLLER", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="f88", | ||||||
|  |         backing_type="float", | ||||||
|  |         validation_schema=cv.positive_float, | ||||||
|  |         default_value=0, | ||||||
|  |         order=3, | ||||||
|  |     ), | ||||||
|  |     "controller_configuration": SettingSchema( | ||||||
|  |         description="Controller configuration", | ||||||
|  |         message="CONTROLLER_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="u8_hb", | ||||||
|  |         backing_type="uint8_t", | ||||||
|  |         validation_schema=cv.int_range(min=0, max=255), | ||||||
|  |         default_value=0, | ||||||
|  |         order=5, | ||||||
|  |     ), | ||||||
|  |     "controller_id": SettingSchema( | ||||||
|  |         description="Controller ID code", | ||||||
|  |         message="CONTROLLER_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="u8_lb", | ||||||
|  |         backing_type="uint8_t", | ||||||
|  |         validation_schema=cv.int_range(min=0, max=255), | ||||||
|  |         default_value=0, | ||||||
|  |         order=5, | ||||||
|  |     ), | ||||||
|  | } | ||||||
|   | |||||||
| @@ -9,12 +9,17 @@ from .schema import TSchema | |||||||
|  |  | ||||||
|  |  | ||||||
| def create_entities_schema( | def create_entities_schema( | ||||||
|     entities: dict[str, schema.EntitySchema], |     entities: dict[str, TSchema], | ||||||
|     get_entity_validation_schema: Callable[[TSchema], cv.Schema], |     get_entity_validation_schema: Callable[[TSchema], cv.Schema], | ||||||
| ) -> Schema: | ) -> Schema: | ||||||
|     entity_schema = {} |     entity_schema = {} | ||||||
|     for key, entity in entities.items(): |     for key, entity in entities.items(): | ||||||
|         entity_schema[cv.Optional(key)] = get_entity_validation_schema(entity) |         schema_key = ( | ||||||
|  |             cv.Optional(key, entity.default_value) | ||||||
|  |             if hasattr(entity, "default_value") | ||||||
|  |             else cv.Optional(key) | ||||||
|  |         ) | ||||||
|  |         entity_schema[schema_key] = get_entity_validation_schema(entity) | ||||||
|     return cv.Schema(entity_schema) |     return cv.Schema(entity_schema) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,6 +16,19 @@ opentherm: | |||||||
|   summer_mode_active: true |   summer_mode_active: true | ||||||
|   dhw_block: true |   dhw_block: true | ||||||
|   sync_mode: true |   sync_mode: true | ||||||
|  |   controller_product_type: 63 | ||||||
|  |   controller_product_version: 1 | ||||||
|  |   opentherm_version_controller: 2.2 | ||||||
|  |   controller_id: 1 | ||||||
|  |   controller_configuration: 1 | ||||||
|  |   before_send: | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           ESP_LOGW("OT", ">> Sending message %d", x.id); | ||||||
|  |   before_process_response: | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           ESP_LOGW("OT", "<< Processing response %d", x.id); | ||||||
|  |  | ||||||
| output: | output: | ||||||
|   - platform: opentherm |   - platform: opentherm | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user