mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +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 | ||||
|  | ||||
| import logging | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| 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 | ||||
|  | ||||
| CODEOWNERS = ["@olegtarasov"] | ||||
| @@ -20,7 +22,21 @@ CONF_CH2_ACTIVE = "ch2_active" | ||||
| CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" | ||||
| CONF_DHW_BLOCK = "dhw_block" | ||||
| 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( | ||||
|     cv.Schema( | ||||
| @@ -36,7 +52,19 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, | ||||
|             cv.Optional(CONF_DHW_BLOCK, 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( | ||||
| @@ -44,6 +72,11 @@ CONFIG_SCHEMA = cv.All( | ||||
|             schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) | ||||
|         ) | ||||
|     ) | ||||
|     .extend( | ||||
|         validate.create_entities_schema( | ||||
|             schema.SETTINGS, (lambda s: s.validation_schema) | ||||
|         ) | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA), | ||||
|     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]) | ||||
|     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 = [] | ||||
|     settings = [] | ||||
|     for key, value in config.items(): | ||||
|         if key in non_sensors: | ||||
|             continue | ||||
|         if key in schema.INPUTS: | ||||
|             input_sensor = await cg.get_variable(value) | ||||
|             cg.add( | ||||
|                 getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor) | ||||
|             ) | ||||
|             cg.add(getattr(var, f"set_{key}_{const.INPUT_SENSOR}")(input_sensor)) | ||||
|             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: | ||||
|             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)) | ||||
|  | ||||
|     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.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" | ||||
| OUTPUT = "output" | ||||
| INPUT_SENSOR = "input_sensor" | ||||
| SETTING = "setting" | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| from collections.abc import Awaitable | ||||
| from typing import Any, Callable | ||||
| from typing import Any, Callable, Optional | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import CONF_ID | ||||
| from . import const | ||||
| from .schema import TSchema | ||||
| from .schema import TSchema, SettingSchema | ||||
|  | ||||
| opentherm_ns = cg.esphome_ns.namespace("opentherm") | ||||
| OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) | ||||
| OpenthermData = opentherm_ns.class_("OpenthermData") | ||||
|  | ||||
|  | ||||
| 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}") | ||||
|  | ||||
|  | ||||
| # 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( | ||||
|     component_type: str, keys: list[str], schemas: dict[str, TSchema] | ||||
| ) -> None: | ||||
| @@ -74,14 +93,28 @@ def define_readers(component_type: str, keys: list[str]) -> None: | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): | ||||
|     messages: set[tuple[str, bool]] = set() | ||||
| def define_setting_readers(component_type: str, keys: list[str]) -> None: | ||||
|     for key in keys: | ||||
|         messages.add((schemas[key].message, schemas[key].keep_updated)) | ||||
|     for msg, keep_updated in messages: | ||||
|         cg.add_define( | ||||
|             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}") | ||||
|         if keep_updated: | ||||
|             cg.add(hub.add_repeating_message(msg_expr)) | ||||
|         else: | ||||
|             if order is not None: | ||||
|                 cg.add(hub.add_initial_message(msg_expr, order)) | ||||
|             else: | ||||
|                 cg.add(hub.add_initial_message(msg_expr)) | ||||
|  | ||||
|   | ||||
| @@ -63,7 +63,7 @@ void write_f88(const float value, OpenthermData &data) { data.f88(value); } | ||||
| OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | ||||
|   OpenthermData data; | ||||
|   data.type = 0; | ||||
|   data.id = 0; | ||||
|   data.id = request_id; | ||||
|   data.valueHB = 0; | ||||
|   data.valueLB = 0; | ||||
|  | ||||
| @@ -82,28 +82,13 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | ||||
|     // NOLINTEND | ||||
|  | ||||
|     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) | | ||||
|                    (summer_mode_is_active << 5) | (dhw_blocked << 6); | ||||
|  | ||||
|     return data; | ||||
|   } | ||||
|  | ||||
|   // Another special case is OpenTherm version number which is configured at hub level as a constant | ||||
|   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, | ||||
|   // Next, we start with write requests from switches and other inputs, | ||||
|   // 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 | ||||
|   // supported). | ||||
| @@ -116,14 +101,23 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | ||||
|                                       OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||
|     OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , | ||||
|                                             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. | ||||
|   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) { | ||||
|     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. | ||||
|   // 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 | ||||
|   // good practice anyway. | ||||
|   this->add_repeating_message(MessageId::STATUS); | ||||
|  | ||||
|   // Also ensure that we start communication with the STATUS message | ||||
|   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(); | ||||
|   this->write_initial_messages_(this->messages_); | ||||
|   this->message_iterator_ = this->messages_.begin(); | ||||
| } | ||||
|  | ||||
| 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() { | ||||
|   if (this->sync_mode_) { | ||||
|     this->sync_loop_(); | ||||
| @@ -184,29 +196,18 @@ void OpenthermHub::loop() { | ||||
|  | ||||
|   auto cur_time = millis(); | ||||
|   auto const cur_mode = this->opentherm_->get_mode(); | ||||
|  | ||||
|   if (this->handle_error_(cur_mode)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   switch (cur_mode) { | ||||
|     case OperationMode::WRITE: | ||||
|     case OperationMode::READ: | ||||
|     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; | ||||
|     case OperationMode::IDLE: | ||||
|       this->check_timings_(cur_time); | ||||
|       if (this->should_skip_loop_(cur_time)) { | ||||
|         break; | ||||
|       } | ||||
| @@ -219,6 +220,28 @@ void OpenthermHub::loop() { | ||||
|     case OperationMode::RECEIVED: | ||||
|       this->read_response_(); | ||||
|       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_(); | ||||
|   // 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(); })) { | ||||
|     ESP_LOGE(TAG, "Hub timeout triggered during send"); | ||||
|     this->stop_opentherm_(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->opentherm_->is_error()) { | ||||
|     this->handle_protocol_write_error_(); | ||||
|     this->stop_opentherm_(); | ||||
|   // Check for errors and ensure we are in the right state (message sent successfully) | ||||
|   if (this->handle_error_(this->opentherm_->get_mode())) { | ||||
|     return; | ||||
|   } else if (!this->opentherm_->is_sent()) { | ||||
|     ESP_LOGW(TAG, "Unexpected state after sending request: %s", | ||||
| @@ -257,19 +284,20 @@ void OpenthermHub::sync_loop_() { | ||||
|  | ||||
|   // Listen for the response | ||||
|   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(); })) { | ||||
|     ESP_LOGE(TAG, "Hub timeout triggered during receive"); | ||||
|     this->stop_opentherm_(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->opentherm_->is_timeout()) { | ||||
|     this->handle_timeout_error_(); | ||||
|     this->stop_opentherm_(); | ||||
|     return; | ||||
|   } else if (this->opentherm_->is_protocol_error()) { | ||||
|     this->handle_protocol_read_error_(); | ||||
|     this->stop_opentherm_(); | ||||
|   // Check for errors and ensure we are in the right state (message received successfully) | ||||
|   if (this->handle_error_(this->opentherm_->get_mode())) { | ||||
|     return; | ||||
|   } else if (!this->opentherm_->has_message()) { | ||||
|     ESP_LOGW(TAG, "Unexpected state after receiving response: %s", | ||||
| @@ -281,17 +309,13 @@ void OpenthermHub::sync_loop_() { | ||||
|   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) { | ||||
|     ESP_LOGW(TAG, | ||||
|              "%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.", | ||||
|              (int) (cur_time - this->last_conversation_start_)); | ||||
|     this->stop_opentherm_(); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| 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_() { | ||||
|   if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { | ||||
|   if (this->message_iterator_ == this->messages_.end()) { | ||||
|     if (this->sending_initial_) { | ||||
|       this->sending_initial_ = false; | ||||
|     this->current_message_iterator_ = this->repeating_messages_.begin(); | ||||
|   } else if (this->current_message_iterator_ == this->repeating_messages_.end()) { | ||||
|     this->current_message_iterator_ = this->repeating_messages_.begin(); | ||||
|       this->write_repeating_messages_(this->messages_); | ||||
|     } | ||||
|     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, | ||||
|            this->opentherm_->message_id_to_str((MessageId) request.id)); | ||||
| @@ -331,37 +358,48 @@ void OpenthermHub::read_response_() { | ||||
|  | ||||
|   this->stop_opentherm_(); | ||||
|  | ||||
|   this->before_process_response_callback_.call(response); | ||||
|   this->process_response(response); | ||||
|  | ||||
|   this->current_message_iterator_++; | ||||
|   this->message_iterator_++; | ||||
| } | ||||
|  | ||||
| void OpenthermHub::stop_opentherm_() { | ||||
|   this->opentherm_->stop(); | ||||
|   this->last_conversation_end_ = millis(); | ||||
| } | ||||
| void OpenthermHub::handle_protocol_write_error_() { | ||||
|   ESP_LOGW(TAG, "Error while sending request: %s", | ||||
|            this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); | ||||
|   this->opentherm_->debug_data(this->last_request_); | ||||
| } | ||||
| void OpenthermHub::handle_protocol_read_error_() { | ||||
|  | ||||
| void OpenthermHub::handle_protocol_error_() { | ||||
|   OpenThermError error; | ||||
|   this->opentherm_->get_protocol_error(error); | ||||
|   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); | ||||
| } | ||||
| void OpenthermHub::handle_timeout_error_() { | ||||
|   ESP_LOGW(TAG, "Receive response timed out at a protocol level"); | ||||
|   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() { | ||||
|   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:"); | ||||
|   LOG_PIN("  In: ", this->in_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, "  Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_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, "  Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); | ||||
|   ESP_LOGCONFIG(TAG, "  Initial requests:"); | ||||
|   for (auto type : this->initial_messages_) { | ||||
|     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str((type))); | ||||
|   for (auto type : initial_messages) { | ||||
|     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str(type)); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  Repeating requests:"); | ||||
|   for (auto type : this->repeating_messages_) { | ||||
|     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str((type))); | ||||
|   for (auto type : repeating_messages) { | ||||
|     ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str(type)); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -38,6 +38,9 @@ | ||||
| namespace esphome { | ||||
| namespace opentherm { | ||||
|  | ||||
| static const uint8_t REPEATING_MESSAGE_ORDER = 255; | ||||
| static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254; | ||||
|  | ||||
| // OpenTherm component for ESPHome | ||||
| class OpenthermHub : public Component { | ||||
|  protected: | ||||
| @@ -58,15 +61,12 @@ class OpenthermHub : public Component { | ||||
|  | ||||
|   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) | ||||
|  | ||||
|   // The set of initial messages to send on starting communication with the boiler | ||||
|   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 | ||||
|   OPENTHERM_SETTING_LIST(OPENTHERM_DECLARE_SETTING, ) | ||||
|  | ||||
|   bool sending_initial_ = true; | ||||
|   // Index for the current request in one of the _requests sets. | ||||
|   std::vector<MessageId>::const_iterator current_message_iterator_; | ||||
|   std::unordered_map<MessageId, uint8_t> configured_messages_; | ||||
|   std::vector<MessageId> messages_; | ||||
|   std::vector<MessageId>::const_iterator message_iterator_; | ||||
|  | ||||
|   uint32_t last_conversation_start_ = 0; | ||||
|   uint32_t last_conversation_end_ = 0; | ||||
| @@ -78,20 +78,25 @@ class OpenthermHub : public Component { | ||||
|   // Very likely to happen while using Dallas temperature sensors. | ||||
|   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 | ||||
|   OpenthermData build_request_(MessageId request_id) const; | ||||
|   void handle_protocol_write_error_(); | ||||
|   void handle_protocol_read_error_(); | ||||
|   bool handle_error_(OperationMode mode); | ||||
|   void handle_protocol_error_(); | ||||
|   void handle_timeout_error_(); | ||||
|   void handle_timer_error_(); | ||||
|   void stop_opentherm_(); | ||||
|   void start_conversation_(); | ||||
|   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; | ||||
|   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) { | ||||
|     auto start_time = millis(); | ||||
|     while (func()) { | ||||
| @@ -127,13 +132,18 @@ class OpenthermHub : public Component { | ||||
|  | ||||
|   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) | ||||
|  | ||||
|   OPENTHERM_SETTING_LIST(OPENTHERM_SET_SETTING, ) | ||||
|  | ||||
|   // 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 | ||||
|   // 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 | ||||
|   // 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, | ||||
|   // 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_dhw_block(bool value) { this->dhw_block = value; } | ||||
|   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; } | ||||
|  | ||||
|   | ||||
| @@ -52,7 +52,9 @@ bool OpenTherm::initialize() { | ||||
|   OpenTherm::instance = this; | ||||
| #endif | ||||
|   this->in_pin_->pin_mode(gpio::FLAG_INPUT); | ||||
|   this->in_pin_->setup(); | ||||
|   this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
|   this->out_pin_->setup(); | ||||
|   this->out_pin_->digital_write(true); | ||||
|  | ||||
| #if defined(ESP32) || defined(USE_ESP_IDF) | ||||
| @@ -182,7 +184,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) { | ||||
|       } | ||||
|       arg->capture_ = 1;  // reset counter | ||||
|     } 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->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; | ||||
|       arg->stop_timer_(); | ||||
| @@ -312,21 +314,31 @@ bool OpenTherm::init_esp32_timer_() { | ||||
| } | ||||
|  | ||||
| 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); | ||||
|   if (result != ESP_OK) { | ||||
|     const auto *error = esp_err_to_name(result); | ||||
|     ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error); | ||||
|   this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); | ||||
|   if (this->timer_error_ != ESP_OK) { | ||||
|     this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_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; | ||||
|   } | ||||
|  | ||||
|   result = timer_start(this->timer_group_, this->timer_idx_); | ||||
|   if (result != ESP_OK) { | ||||
|     const auto *error = esp_err_to_name(result); | ||||
|     ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); | ||||
|     return; | ||||
|   } | ||||
|   ESP_LOGE(TAG, "Error occured while manipulating timer (%s): %s", this->timer_error_to_str(this->timer_error_type_), | ||||
|            esp_err_to_name(this->timer_error_)); | ||||
|  | ||||
|   this->timer_error_ = ESP_OK; | ||||
|   this->timer_error_type_ = NO_TIMER_ERROR; | ||||
| } | ||||
|  | ||||
| // 5 kHz timer_ | ||||
| @@ -343,21 +355,18 @@ void IRAM_ATTR OpenTherm::start_write_timer_() { | ||||
|  | ||||
| void IRAM_ATTR OpenTherm::stop_timer_() { | ||||
|   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; | ||||
|  | ||||
|   result = timer_pause(this->timer_group_, this->timer_idx_); | ||||
|   if (result != ESP_OK) { | ||||
|     const auto *error = esp_err_to_name(result); | ||||
|     ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error); | ||||
|   this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_); | ||||
|   if (this->timer_error_ != ESP_OK) { | ||||
|     this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR; | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); | ||||
|   if (result != ESP_OK) { | ||||
|     const auto *error = esp_err_to_name(result); | ||||
|     ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error); | ||||
|     return; | ||||
|   this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); | ||||
|   if (this->timer_error_ != ESP_OK) { | ||||
|     this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -386,6 +395,9 @@ void IRAM_ATTR OpenTherm::stop_timer_() { | ||||
|   timer1_detachInterrupt(); | ||||
| } | ||||
|  | ||||
| // There is nothing to report on ESP8266 | ||||
| void OpenTherm::report_and_reset_timer_error() {} | ||||
|  | ||||
| #endif  // END ESP8266 | ||||
|  | ||||
| // 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(ERROR_PROTOCOL) | ||||
|     TO_STRING_MEMBER(ERROR_TIMEOUT) | ||||
|     TO_STRING_MEMBER(ERROR_TIMER) | ||||
|     default: | ||||
|       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) { | ||||
|     TO_STRING_MEMBER(NO_ERROR) | ||||
|     TO_STRING_MEMBER(NO_TRANSITION) | ||||
| @@ -427,6 +440,17 @@ const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { | ||||
|       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) { | ||||
|   switch (message_type) { | ||||
|     TO_STRING_MEMBER(READ_DATA) | ||||
|   | ||||
| @@ -36,11 +36,12 @@ enum OperationMode { | ||||
|   READ = 2,      // reading 32-bit data frame | ||||
|   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 | ||||
|  | ||||
|   ERROR_PROTOCOL = 8,  // manchester protocol data transfer error | ||||
|   ERROR_TIMEOUT = 9    // read timeout | ||||
|   ERROR_PROTOCOL = 8,  // protocol error, can happed only during READ | ||||
|   ERROR_TIMEOUT = 9,   // timeout while waiting for response from device, only during LISTEN | ||||
|   ERROR_TIMER = 10     // error operating the ESP32 timer | ||||
| }; | ||||
|  | ||||
| enum ProtocolErrorType { | ||||
| @@ -51,6 +52,14 @@ enum ProtocolErrorType { | ||||
|   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 { | ||||
|   READ_DATA = 0, | ||||
|   READ_ACK = 4, | ||||
| @@ -299,7 +308,9 @@ class OpenTherm { | ||||
|    * | ||||
|    * @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 | ||||
| @@ -313,14 +324,22 @@ class OpenTherm { | ||||
|    */ | ||||
|   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; } | ||||
|  | ||||
|   OperationMode get_mode() { return mode_; } | ||||
|  | ||||
|   void debug_data(OpenthermData &data); | ||||
|   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 *operation_mode_to_str(OperationMode mode); | ||||
|   const char *message_id_to_str(MessageId id); | ||||
| @@ -349,10 +368,12 @@ class OpenTherm { | ||||
|   uint32_t data_; | ||||
|   uint8_t bit_pos_; | ||||
|   int32_t timeout_counter_;  // <0 no timeout | ||||
|  | ||||
|   int32_t device_timeout_; | ||||
|  | ||||
| #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_(); | ||||
|   void start_esp32_timer_(uint64_t alarm_value); | ||||
| #endif | ||||
|   | ||||
| @@ -28,6 +28,9 @@ namespace opentherm { | ||||
| #ifndef OPENTHERM_INPUT_SENSOR_LIST | ||||
| #define OPENTHERM_INPUT_SENSOR_LIST(F, sep) | ||||
| #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 | ||||
| #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; | ||||
| @@ -36,6 +39,7 @@ namespace opentherm { | ||||
| #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; | ||||
| #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; | ||||
| #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; | ||||
| #define OPENTHERM_DECLARE_SETTING(type, entity, def) type entity = def; | ||||
|  | ||||
| // Setter macros | ||||
| #define OPENTHERM_SET_SENSOR(entity) \ | ||||
| @@ -56,6 +60,9 @@ namespace opentherm { | ||||
| #define OPENTHERM_SET_INPUT_SENSOR(entity) \ | ||||
|   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 ===== | ||||
|  | ||||
| // *_MESSAGE_HANDLERS are generated in defines.h and look like this: | ||||
| @@ -85,6 +92,9 @@ namespace opentherm { | ||||
| #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS | ||||
| #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||
| #endif | ||||
| #ifndef OPENTHERM_SETTING_MESSAGE_HANDLERS | ||||
| #define OPENTHERM_SETTING_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||
| #endif | ||||
|  | ||||
| // Write data request builders | ||||
| #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ | ||||
| @@ -92,6 +102,7 @@ namespace opentherm { | ||||
|     data.type = MessageType::WRITE_DATA; \ | ||||
|     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_SETTING(key, msg_data) message_data::write_##msg_data(this->key, data); | ||||
| #define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \ | ||||
|   return data; \ | ||||
|   } | ||||
|   | ||||
| @@ -2,8 +2,9 @@ | ||||
| # inputs of the OpenTherm component. | ||||
|  | ||||
| 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 ( | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_EMPTY, | ||||
| @@ -64,6 +65,7 @@ class SensorSchema(EntitySchema): | ||||
|     icon: Optional[str] = None | ||||
|     device_class: Optional[str] = None | ||||
|     disabled_by_default: bool = False | ||||
|     order: Optional[int] = None | ||||
|  | ||||
|  | ||||
| SENSORS: dict[str, SensorSchema] = { | ||||
| @@ -399,6 +401,7 @@ SENSORS: dict[str, SensorSchema] = { | ||||
|         message="OT_VERSION_DEVICE", | ||||
|         keep_updated=False, | ||||
|         message_data="f88", | ||||
|         order=2, | ||||
|     ), | ||||
|     "device_type": SensorSchema( | ||||
|         description="Device product type", | ||||
| @@ -409,6 +412,7 @@ SENSORS: dict[str, SensorSchema] = { | ||||
|         message="VERSION_DEVICE", | ||||
|         keep_updated=False, | ||||
|         message_data="u8_hb", | ||||
|         order=0, | ||||
|     ), | ||||
|     "device_version": SensorSchema( | ||||
|         description="Device product version", | ||||
| @@ -419,6 +423,7 @@ SENSORS: dict[str, SensorSchema] = { | ||||
|         message="VERSION_DEVICE", | ||||
|         keep_updated=False, | ||||
|         message_data="u8_lb", | ||||
|         order=0, | ||||
|     ), | ||||
|     "device_id": SensorSchema( | ||||
|         description="Device ID code", | ||||
| @@ -429,6 +434,7 @@ SENSORS: dict[str, SensorSchema] = { | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="u8_lb", | ||||
|         order=4, | ||||
|     ), | ||||
|     "otc_hc_ratio_ub": SensorSchema( | ||||
|         description="OTC heat curve ratio upper bound", | ||||
| @@ -457,6 +463,7 @@ SENSORS: dict[str, SensorSchema] = { | ||||
| class BinarySensorSchema(EntitySchema): | ||||
|     icon: Optional[str] = None | ||||
|     device_class: Optional[str] = None | ||||
|     order: Optional[int] = None | ||||
|  | ||||
|  | ||||
| BINARY_SENSORS: dict[str, BinarySensorSchema] = { | ||||
| @@ -525,48 +532,56 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = { | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_0", | ||||
|         order=4, | ||||
|     ), | ||||
|     "control_type_on_off": BinarySensorSchema( | ||||
|         description="Configuration: Control type is on/off", | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_1", | ||||
|         order=4, | ||||
|     ), | ||||
|     "cooling_supported": BinarySensorSchema( | ||||
|         description="Configuration: Cooling supported", | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_2", | ||||
|         order=4, | ||||
|     ), | ||||
|     "dhw_storage_tank": BinarySensorSchema( | ||||
|         description="Configuration: DHW storage tank", | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_3", | ||||
|         order=4, | ||||
|     ), | ||||
|     "controller_pump_control_allowed": BinarySensorSchema( | ||||
|         description="Configuration: Controller pump control allowed", | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_4", | ||||
|         order=4, | ||||
|     ), | ||||
|     "ch2_present": BinarySensorSchema( | ||||
|         description="Configuration: CH2 present", | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_5", | ||||
|         order=4, | ||||
|     ), | ||||
|     "water_filling": BinarySensorSchema( | ||||
|         description="Configuration: Remote water filling", | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_6", | ||||
|         order=4, | ||||
|     ), | ||||
|     "heat_mode": BinarySensorSchema( | ||||
|         description="Configuration: Heating or cooling", | ||||
|         message="DEVICE_CONFIG", | ||||
|         keep_updated=False, | ||||
|         message_data="flag8_hb_7", | ||||
|         order=4, | ||||
|     ), | ||||
|     "dhw_setpoint_transfer_enabled": BinarySensorSchema( | ||||
|         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"), | ||||
|     ), | ||||
| } | ||||
|  | ||||
|  | ||||
| @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( | ||||
|     entities: dict[str, schema.EntitySchema], | ||||
|     entities: dict[str, TSchema], | ||||
|     get_entity_validation_schema: Callable[[TSchema], cv.Schema], | ||||
| ) -> Schema: | ||||
|     entity_schema = {} | ||||
|     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) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,19 @@ opentherm: | ||||
|   summer_mode_active: true | ||||
|   dhw_block: 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: | ||||
|   - platform: opentherm | ||||
|   | ||||
		Reference in New Issue
	
	Block a user