mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Add OpenTherm component (part 3: rest of the sensors) (#7676)
Co-authored-by: FreeBear <freebear@tuxcnc.org> Co-authored-by: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -3,8 +3,9 @@ from typing import Any | |||||||
| 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.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 | from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 | ||||||
| from . import generate | from . import const, schema, validate, generate | ||||||
|  |  | ||||||
| CODEOWNERS = ["@olegtarasov"] | CODEOWNERS = ["@olegtarasov"] | ||||||
| MULTI_CONF = True | MULTI_CONF = True | ||||||
| @@ -19,6 +20,7 @@ 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" | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
| @@ -34,8 +36,15 @@ 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, | ||||||
|         } |         } | ||||||
|     ).extend(cv.COMPONENT_SCHEMA), |     ) | ||||||
|  |     .extend( | ||||||
|  |         validate.create_entities_schema( | ||||||
|  |             schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |     .extend(cv.COMPONENT_SCHEMA), | ||||||
|     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), |     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -52,8 +61,23 @@ async def to_code(config: dict[str, Any]) -> None: | |||||||
|     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} | ||||||
|  |     input_sensors = [] | ||||||
|     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: | ||||||
|  |             input_sensor = await cg.get_variable(value) | ||||||
|  |             cg.add( | ||||||
|  |                 getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor) | ||||||
|  |             ) | ||||||
|  |             input_sensors.append(key) | ||||||
|  |         else: | ||||||
|             cg.add(getattr(var, f"set_{key}")(value)) |             cg.add(getattr(var, f"set_{key}")(value)) | ||||||
|  |  | ||||||
|  |     if len(input_sensors) > 0: | ||||||
|  |         generate.define_has_component(const.INPUT_SENSOR, input_sensors) | ||||||
|  |         generate.define_message_handler( | ||||||
|  |             const.INPUT_SENSOR, input_sensors, schema.INPUTS | ||||||
|  |         ) | ||||||
|  |         generate.define_readers(const.INPUT_SENSOR, input_sensors) | ||||||
|  |         generate.add_messages(var, input_sensors, schema.INPUTS) | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								esphome/components/opentherm/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/opentherm/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import binary_sensor | ||||||
|  | from .. import const, schema, validate, generate | ||||||
|  |  | ||||||
|  | DEPENDENCIES = [const.OPENTHERM] | ||||||
|  | COMPONENT_TYPE = const.BINARY_SENSOR | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema: | ||||||
|  |     return binary_sensor.binary_sensor_schema( | ||||||
|  |         device_class=( | ||||||
|  |             entity.device_class | ||||||
|  |             or binary_sensor._UNDEF  # pylint: disable=protected-access | ||||||
|  |         ), | ||||||
|  |         icon=(entity.icon or binary_sensor._UNDEF),  # pylint: disable=protected-access | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = validate.create_component_schema( | ||||||
|  |     schema.BINARY_SENSORS, get_entity_validation_schema | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config: dict[str, Any]) -> None: | ||||||
|  |     await generate.component_to_code( | ||||||
|  |         COMPONENT_TYPE, | ||||||
|  |         schema.BINARY_SENSORS, | ||||||
|  |         binary_sensor.BinarySensor, | ||||||
|  |         generate.create_only_conf(binary_sensor.new_binary_sensor), | ||||||
|  |         config, | ||||||
|  |     ) | ||||||
| @@ -1,5 +1,11 @@ | |||||||
| OPENTHERM = "opentherm" | OPENTHERM = "opentherm" | ||||||
|  |  | ||||||
| CONF_OPENTHERM_ID = "opentherm_id" | CONF_OPENTHERM_ID = "opentherm_id" | ||||||
|  | CONF_DATA_TYPE = "data_type" | ||||||
|  |  | ||||||
| SENSOR = "sensor" | SENSOR = "sensor" | ||||||
|  | BINARY_SENSOR = "binary_sensor" | ||||||
|  | SWITCH = "switch" | ||||||
|  | NUMBER = "number" | ||||||
|  | OUTPUT = "output" | ||||||
|  | INPUT_SENSOR = "input_sensor" | ||||||
|   | |||||||
| @@ -130,6 +130,8 @@ async def component_to_code( | |||||||
|         id = conf[CONF_ID] |         id = conf[CONF_ID] | ||||||
|         if id and id.type == type: |         if id and id.type == type: | ||||||
|             entity = await create(conf, key, hub) |             entity = await create(conf, key, hub) | ||||||
|  |             if const.CONF_DATA_TYPE in conf: | ||||||
|  |                 schemas[key].message_data = conf[const.CONF_DATA_TYPE] | ||||||
|             cg.add(getattr(hub, f"set_{key}_{component_type.lower()}")(entity)) |             cg.add(getattr(hub, f"set_{key}_{component_type.lower()}")(entity)) | ||||||
|             keys.append(key) |             keys.append(key) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -29,6 +29,8 @@ uint8_t parse_u8_hb(OpenthermData &data) { return data.valueHB; } | |||||||
| int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; } | int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; } | ||||||
| int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; } | int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; } | ||||||
| uint16_t parse_u16(OpenthermData &data) { return data.u16(); } | uint16_t parse_u16(OpenthermData &data) { return data.u16(); } | ||||||
|  | uint16_t parse_u8_lb_60(OpenthermData &data) { return data.valueLB * 60; } | ||||||
|  | uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; } | ||||||
| int16_t parse_s16(OpenthermData &data) { return data.s16(); } | int16_t parse_s16(OpenthermData &data) { return data.s16(); } | ||||||
| float parse_f88(OpenthermData &data) { return data.f88(); } | float parse_f88(OpenthermData &data) { return data.f88(); } | ||||||
|  |  | ||||||
| @@ -87,13 +89,40 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { | |||||||
|     return data; |     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 | // Disable incomplete switch statement warnings, because the cases in each | ||||||
| // switch are generated based on the configured sensors and inputs. | // switch are generated based on the configured sensors and inputs. | ||||||
| #pragma GCC diagnostic push | #pragma GCC diagnostic push | ||||||
| #pragma GCC diagnostic ignored "-Wswitch" | #pragma GCC diagnostic ignored "-Wswitch" | ||||||
|  |  | ||||||
|   switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } |   // 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 | ||||||
|  |   // request a read for that type (in the case that both read and write are | ||||||
|  |   // supported). | ||||||
|  |   switch (request_id) { | ||||||
|  |     OPENTHERM_SWITCH_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , | ||||||
|  |                                       OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||||
|  |     OPENTHERM_NUMBER_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , | ||||||
|  |                                       OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||||
|  |     OPENTHERM_OUTPUT_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , | ||||||
|  |                                       OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||||
|  |     OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , | ||||||
|  |                                             OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 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_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) | ||||||
|  |   } | ||||||
| #pragma GCC diagnostic pop | #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. | ||||||
| @@ -115,6 +144,10 @@ void OpenthermHub::process_response(OpenthermData &data) { | |||||||
|     OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, , |     OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, , | ||||||
|                                       OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, ) |                                       OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, ) | ||||||
|   } |   } | ||||||
|  |   switch (data.id) { | ||||||
|  |     OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, , | ||||||
|  |                                              OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, ) | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void OpenthermHub::setup() { | void OpenthermHub::setup() { | ||||||
| @@ -131,6 +164,13 @@ void OpenthermHub::setup() { | |||||||
|   // good practice anyway. |   // good practice anyway. | ||||||
|   this->add_repeating_message(MessageId::STATUS); |   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->current_message_iterator_ = this->initial_messages_.begin(); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| #include "opentherm.h" | #include "opentherm.h" | ||||||
|  |  | ||||||
| @@ -11,6 +12,22 @@ | |||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef OPENTHERM_USE_BINARY_SENSOR | ||||||
|  | #include "esphome/components/binary_sensor/binary_sensor.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef OPENTHERM_USE_SWITCH | ||||||
|  | #include "esphome/components/opentherm/switch/switch.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef OPENTHERM_USE_OUTPUT | ||||||
|  | #include "esphome/components/opentherm/output/output.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef OPENTHERM_USE_NUMBER | ||||||
|  | #include "esphome/components/opentherm/number/number.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <unordered_map> | #include <unordered_map> | ||||||
| #include <unordered_set> | #include <unordered_set> | ||||||
| @@ -31,15 +48,25 @@ class OpenthermHub : public Component { | |||||||
|  |  | ||||||
|   OPENTHERM_SENSOR_LIST(OPENTHERM_DECLARE_SENSOR, ) |   OPENTHERM_SENSOR_LIST(OPENTHERM_DECLARE_SENSOR, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_BINARY_SENSOR_LIST(OPENTHERM_DECLARE_BINARY_SENSOR, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_SWITCH_LIST(OPENTHERM_DECLARE_SWITCH, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_NUMBER_LIST(OPENTHERM_DECLARE_NUMBER, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_OUTPUT_LIST(OPENTHERM_DECLARE_OUTPUT, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) | ||||||
|  |  | ||||||
|   // The set of initial messages to send on starting communication with the boiler |   // The set of initial messages to send on starting communication with the boiler | ||||||
|   std::unordered_set<MessageId> initial_messages_; |   std::vector<MessageId> initial_messages_; | ||||||
|   // and the repeating messages which are sent repeatedly to update various sensors |   // and the repeating messages which are sent repeatedly to update various sensors | ||||||
|   // and boiler parameters (like the setpoint). |   // and boiler parameters (like the setpoint). | ||||||
|   std::unordered_set<MessageId> repeating_messages_; |   std::vector<MessageId> repeating_messages_; | ||||||
|   // Indicates if we are still working on the initial requests or not |   // 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. |   // Index for the current request in one of the _requests sets. | ||||||
|   std::unordered_set<MessageId>::const_iterator current_message_iterator_; |   std::vector<MessageId>::const_iterator current_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; | ||||||
| @@ -51,6 +78,8 @@ 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; | ||||||
|  |  | ||||||
|   // 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_(); |   void handle_protocol_write_error_(); | ||||||
| @@ -88,13 +117,23 @@ class OpenthermHub : public Component { | |||||||
|  |  | ||||||
|   OPENTHERM_SENSOR_LIST(OPENTHERM_SET_SENSOR, ) |   OPENTHERM_SENSOR_LIST(OPENTHERM_SET_SENSOR, ) | ||||||
|  |  | ||||||
|   // Add a request to the set of initial requests |   OPENTHERM_BINARY_SENSOR_LIST(OPENTHERM_SET_BINARY_SENSOR, ) | ||||||
|   void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); } |  | ||||||
|  |   OPENTHERM_SWITCH_LIST(OPENTHERM_SET_SWITCH, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_NUMBER_LIST(OPENTHERM_SET_NUMBER, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_OUTPUT_LIST(OPENTHERM_SET_OUTPUT, ) | ||||||
|  |  | ||||||
|  |   OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) | ||||||
|  |  | ||||||
|  |   // Add a request to the vector of initial requests | ||||||
|  |   void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); } | ||||||
|   // 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_.insert(message_id); } |   void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); } | ||||||
|  |  | ||||||
|   // 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. | ||||||
| @@ -110,6 +149,7 @@ 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; } | ||||||
|  |  | ||||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } |   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								esphome/components/opentherm/input.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/opentherm/input.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | class OpenthermInput { | ||||||
|  |  public: | ||||||
|  |   bool auto_min_value, auto_max_value; | ||||||
|  |  | ||||||
|  |   virtual void set_min_value(float min_value) = 0; | ||||||
|  |   virtual void set_max_value(float max_value) = 0; | ||||||
|  |  | ||||||
|  |   virtual void set_auto_min_value(bool auto_min_value) { this->auto_min_value = auto_min_value; } | ||||||
|  |   virtual void set_auto_max_value(bool auto_max_value) { this->auto_max_value = auto_max_value; } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										51
									
								
								esphome/components/opentherm/input.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								esphome/components/opentherm/input.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from . import schema, generate | ||||||
|  |  | ||||||
|  | CONF_min_value = "min_value" | ||||||
|  | CONF_max_value = "max_value" | ||||||
|  | CONF_auto_min_value = "auto_min_value" | ||||||
|  | CONF_auto_max_value = "auto_max_value" | ||||||
|  | CONF_step = "step" | ||||||
|  |  | ||||||
|  | OpenthermInput = generate.opentherm_ns.class_("OpenthermInput") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_min_value_less_than_max_value(conf): | ||||||
|  |     if ( | ||||||
|  |         CONF_min_value in conf | ||||||
|  |         and CONF_max_value in conf | ||||||
|  |         and conf[CONF_min_value] > conf[CONF_max_value] | ||||||
|  |     ): | ||||||
|  |         raise cv.Invalid(f"{CONF_min_value} must be less than {CONF_max_value}") | ||||||
|  |     return conf | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def input_schema(entity: schema.InputSchema) -> cv.Schema: | ||||||
|  |     result = cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.Optional(CONF_min_value, entity.range[0]): cv.float_range( | ||||||
|  |                 entity.range[0], entity.range[1] | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_max_value, entity.range[1]): cv.float_range( | ||||||
|  |                 entity.range[0], entity.range[1] | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     result = result.add_extra(validate_min_value_less_than_max_value) | ||||||
|  |     result = result.extend({cv.Optional(CONF_step, False): cv.float_}) | ||||||
|  |     if entity.auto_min_value is not None: | ||||||
|  |         result = result.extend({cv.Optional(CONF_auto_min_value, False): cv.boolean}) | ||||||
|  |     if entity.auto_max_value is not None: | ||||||
|  |         result = result.extend({cv.Optional(CONF_auto_max_value, False): cv.boolean}) | ||||||
|  |  | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def generate_setters(entity: cg.MockObj, conf: dict[str, Any]) -> None: | ||||||
|  |     generate.add_property_set(entity, CONF_min_value, conf) | ||||||
|  |     generate.add_property_set(entity, CONF_max_value, conf) | ||||||
|  |     generate.add_property_set(entity, CONF_auto_min_value, conf) | ||||||
|  |     generate.add_property_set(entity, CONF_auto_max_value, conf) | ||||||
							
								
								
									
										74
									
								
								esphome/components/opentherm/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								esphome/components/opentherm/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import number | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_UNIT_OF_MEASUREMENT, | ||||||
|  |     CONF_STEP, | ||||||
|  |     CONF_INITIAL_VALUE, | ||||||
|  |     CONF_RESTORE_VALUE, | ||||||
|  | ) | ||||||
|  | from .. import const, schema, validate, input, generate | ||||||
|  |  | ||||||
|  | DEPENDENCIES = [const.OPENTHERM] | ||||||
|  | COMPONENT_TYPE = const.NUMBER | ||||||
|  |  | ||||||
|  | OpenthermNumber = generate.opentherm_ns.class_( | ||||||
|  |     "OpenthermNumber", number.Number, cg.Component, input.OpenthermInput | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable: | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await number.register_number( | ||||||
|  |         var, | ||||||
|  |         config, | ||||||
|  |         min_value=config[input.CONF_min_value], | ||||||
|  |         max_value=config[input.CONF_max_value], | ||||||
|  |         step=config[input.CONF_step], | ||||||
|  |     ) | ||||||
|  |     input.generate_setters(var, config) | ||||||
|  |  | ||||||
|  |     if CONF_INITIAL_VALUE in config: | ||||||
|  |         cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) | ||||||
|  |     if CONF_RESTORE_VALUE in config: | ||||||
|  |         cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) | ||||||
|  |  | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_entity_validation_schema(entity: schema.InputSchema) -> cv.Schema: | ||||||
|  |     return ( | ||||||
|  |         number.NUMBER_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.GenerateID(): cv.declare_id(OpenthermNumber), | ||||||
|  |                 cv.Optional( | ||||||
|  |                     CONF_UNIT_OF_MEASUREMENT, entity.unit_of_measurement | ||||||
|  |                 ): cv.string_strict, | ||||||
|  |                 cv.Optional(CONF_STEP, entity.step): cv.float_, | ||||||
|  |                 cv.Optional(CONF_INITIAL_VALUE): cv.float_, | ||||||
|  |                 cv.Optional(CONF_RESTORE_VALUE): cv.boolean, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         .extend(input.input_schema(entity)) | ||||||
|  |         .extend(cv.COMPONENT_SCHEMA) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = validate.create_component_schema( | ||||||
|  |     schema.INPUTS, get_entity_validation_schema | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config: dict[str, Any]) -> None: | ||||||
|  |     keys = await generate.component_to_code( | ||||||
|  |         COMPONENT_TYPE, | ||||||
|  |         schema.INPUTS, | ||||||
|  |         OpenthermNumber, | ||||||
|  |         generate.create_only_conf(new_openthermnumber), | ||||||
|  |         config, | ||||||
|  |     ) | ||||||
|  |     generate.define_readers(COMPONENT_TYPE, keys) | ||||||
							
								
								
									
										40
									
								
								esphome/components/opentherm/number/number.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								esphome/components/opentherm/number/number.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | #include "number.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "opentherm.number"; | ||||||
|  |  | ||||||
|  | void OpenthermNumber::control(float value) { | ||||||
|  |   this->publish_state(value); | ||||||
|  |  | ||||||
|  |   if (this->restore_value_) | ||||||
|  |     this->pref_.save(&value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void OpenthermNumber::setup() { | ||||||
|  |   float value; | ||||||
|  |   if (!this->restore_value_) { | ||||||
|  |     value = this->initial_value_; | ||||||
|  |   } else { | ||||||
|  |     this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash()); | ||||||
|  |     if (!this->pref_.load(&value)) { | ||||||
|  |       if (!std::isnan(this->initial_value_)) { | ||||||
|  |         value = this->initial_value_; | ||||||
|  |       } else { | ||||||
|  |         value = this->traits.get_min_value(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   this->publish_state(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void OpenthermNumber::dump_config() { | ||||||
|  |   LOG_NUMBER("", "OpenTherm Number", this); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Restore value: %d", this->restore_value_); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Initial value: %.2f", this->initial_value_); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Current value: %.2f", this->state); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										31
									
								
								esphome/components/opentherm/number/number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/opentherm/number/number.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/number/number.h" | ||||||
|  | #include "esphome/core/preferences.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/components/opentherm/input.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | // Just a simple number, which stores the number | ||||||
|  | class OpenthermNumber : public number::Number, public Component, public OpenthermInput { | ||||||
|  |  protected: | ||||||
|  |   void control(float value) override; | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   float initial_value_{NAN}; | ||||||
|  |   bool restore_value_{false}; | ||||||
|  |  | ||||||
|  |   ESPPreferenceObject pref_; | ||||||
|  |  | ||||||
|  |  public: | ||||||
|  |   void set_min_value(float min_value) override { this->traits.set_min_value(min_value); } | ||||||
|  |   void set_max_value(float max_value) override { this->traits.set_max_value(max_value); } | ||||||
|  |   void set_initial_value(float initial_value) { initial_value_ = initial_value; } | ||||||
|  |   void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
| @@ -99,6 +99,8 @@ enum MessageId { | |||||||
|   EXHAUST_TEMP = 33, |   EXHAUST_TEMP = 33, | ||||||
|   FAN_SPEED = 35, |   FAN_SPEED = 35, | ||||||
|   FLAME_CURRENT = 36, |   FLAME_CURRENT = 36, | ||||||
|  |   ROOM_TEMP_CH2 = 37, | ||||||
|  |   REL_HUMIDITY = 38, | ||||||
|   DHW_BOUNDS = 48, |   DHW_BOUNDS = 48, | ||||||
|   CH_BOUNDS = 49, |   CH_BOUNDS = 49, | ||||||
|   OTC_CURVE_BOUNDS = 50, |   OTC_CURVE_BOUNDS = 50, | ||||||
| @@ -110,15 +112,46 @@ enum MessageId { | |||||||
|   HVAC_STATUS = 70, |   HVAC_STATUS = 70, | ||||||
|   REL_VENT_SETPOINT = 71, |   REL_VENT_SETPOINT = 71, | ||||||
|   DEVICE_VENT = 74, |   DEVICE_VENT = 74, | ||||||
|  |   HVAC_VER_ID = 75, | ||||||
|   REL_VENTILATION = 77, |   REL_VENTILATION = 77, | ||||||
|   REL_HUMID_EXHAUST = 78, |   REL_HUMID_EXHAUST = 78, | ||||||
|  |   EXHAUST_CO2 = 79, | ||||||
|   SUPPLY_INLET_TEMP = 80, |   SUPPLY_INLET_TEMP = 80, | ||||||
|   SUPPLY_OUTLET_TEMP = 81, |   SUPPLY_OUTLET_TEMP = 81, | ||||||
|   EXHAUST_INLET_TEMP = 82, |   EXHAUST_INLET_TEMP = 82, | ||||||
|   EXHAUST_OUTLET_TEMP = 83, |   EXHAUST_OUTLET_TEMP = 83, | ||||||
|  |   EXHAUST_FAN_SPEED = 84, | ||||||
|  |   SUPPLY_FAN_SPEED = 85, | ||||||
|  |   REMOTE_VENTILATION_PARAM = 86, | ||||||
|   NOM_REL_VENTILATION = 87, |   NOM_REL_VENTILATION = 87, | ||||||
|  |   HVAC_NUM_TSP = 88, | ||||||
|  |   HVAC_IDX_TSP = 89, | ||||||
|  |   HVAC_FHB_SIZE = 90, | ||||||
|  |   HVAC_FHB_IDX = 91, | ||||||
|  |  | ||||||
|  |   RF_SIGNAL = 98, | ||||||
|  |   DHW_MODE = 99, | ||||||
|   OVERRIDE_FUNC = 100, |   OVERRIDE_FUNC = 100, | ||||||
|  |  | ||||||
|  |   // Solar Specific Message IDs | ||||||
|  |   SOLAR_MODE_FLAGS = 101,  // hb0-2 Controller storage mode | ||||||
|  |                            // lb0   Device fault | ||||||
|  |                            // lb1-3 Device mode status | ||||||
|  |                            // lb4-5 Device status | ||||||
|  |   SOLAR_ASF = 102, | ||||||
|  |   SOLAR_VERSION_ID = 103, | ||||||
|  |   SOLAR_PRODUCT_ID = 104, | ||||||
|  |   SOLAR_NUM_TSP = 105, | ||||||
|  |   SOLAR_IDX_TSP = 106, | ||||||
|  |   SOLAR_FHB_SIZE = 107, | ||||||
|  |   SOLAR_FHB_IDX = 108, | ||||||
|  |   SOLAR_STARTS = 109, | ||||||
|  |   SOLAR_HOURS = 110, | ||||||
|  |   SOLAR_ENERGY = 111, | ||||||
|  |   SOLAR_TOTAL_ENERGY = 112, | ||||||
|  |  | ||||||
|  |   FAILED_BURNER_STARTS = 113, | ||||||
|  |   BURNER_FLAME_LOW = 114, | ||||||
|   OEM_DIAGNOSTIC = 115, |   OEM_DIAGNOSTIC = 115, | ||||||
|   BURNER_STARTS = 116, |   BURNER_STARTS = 116, | ||||||
|   CH_PUMP_STARTS = 117, |   CH_PUMP_STARTS = 117, | ||||||
|   | |||||||
| @@ -13,14 +13,49 @@ namespace opentherm { | |||||||
| #ifndef OPENTHERM_SENSOR_LIST | #ifndef OPENTHERM_SENSOR_LIST | ||||||
| #define OPENTHERM_SENSOR_LIST(F, sep) | #define OPENTHERM_SENSOR_LIST(F, sep) | ||||||
| #endif | #endif | ||||||
|  | #ifndef OPENTHERM_BINARY_SENSOR_LIST | ||||||
|  | #define OPENTHERM_BINARY_SENSOR_LIST(F, sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_SWITCH_LIST | ||||||
|  | #define OPENTHERM_SWITCH_LIST(F, sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_NUMBER_LIST | ||||||
|  | #define OPENTHERM_NUMBER_LIST(F, sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_OUTPUT_LIST | ||||||
|  | #define OPENTHERM_OUTPUT_LIST(F, sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_INPUT_SENSOR_LIST | ||||||
|  | #define OPENTHERM_INPUT_SENSOR_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; | ||||||
|  | #define OPENTHERM_DECLARE_BINARY_SENSOR(entity) binary_sensor::BinarySensor *entity; | ||||||
|  | #define OPENTHERM_DECLARE_SWITCH(entity) OpenthermSwitch *entity; | ||||||
|  | #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; | ||||||
|  | #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; | ||||||
|  | #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; | ||||||
|  |  | ||||||
| // Setter macros | // Setter macros | ||||||
| #define OPENTHERM_SET_SENSOR(entity) \ | #define OPENTHERM_SET_SENSOR(entity) \ | ||||||
|   void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } |   void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } | ||||||
|  |  | ||||||
|  | #define OPENTHERM_SET_BINARY_SENSOR(entity) \ | ||||||
|  |   void set_##entity(binary_sensor::BinarySensor *binary_sensor) { this->entity = binary_sensor; } | ||||||
|  |  | ||||||
|  | #define OPENTHERM_SET_SWITCH(entity) \ | ||||||
|  |   void set_##entity(OpenthermSwitch *sw) { this->entity = sw; } | ||||||
|  |  | ||||||
|  | #define OPENTHERM_SET_NUMBER(entity) \ | ||||||
|  |   void set_##entity(OpenthermNumber *number) { this->entity = number; } | ||||||
|  |  | ||||||
|  | #define OPENTHERM_SET_OUTPUT(entity) \ | ||||||
|  |   void set_##entity(OpenthermOutput *output) { this->entity = output; } | ||||||
|  |  | ||||||
|  | #define OPENTHERM_SET_INPUT_SENSOR(entity) \ | ||||||
|  |   void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } | ||||||
|  |  | ||||||
| // ===== 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: | ||||||
| @@ -35,6 +70,31 @@ namespace opentherm { | |||||||
| #ifndef OPENTHERM_SENSOR_MESSAGE_HANDLERS | #ifndef OPENTHERM_SENSOR_MESSAGE_HANDLERS | ||||||
| #define OPENTHERM_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | #define OPENTHERM_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
| #endif | #endif | ||||||
|  | #ifndef OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS | ||||||
|  | #define OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_SWITCH_MESSAGE_HANDLERS | ||||||
|  | #define OPENTHERM_SWITCH_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_NUMBER_MESSAGE_HANDLERS | ||||||
|  | #define OPENTHERM_NUMBER_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_OUTPUT_MESSAGE_HANDLERS | ||||||
|  | #define OPENTHERM_OUTPUT_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
|  | #endif | ||||||
|  | #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS | ||||||
|  | #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | // Write data request builders | ||||||
|  | #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ | ||||||
|  |   case MessageId::msg: { \ | ||||||
|  |     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_POSTSCRIPT \ | ||||||
|  |   return data; \ | ||||||
|  |   } | ||||||
|  |  | ||||||
| // Read data request builder | // Read data request builder | ||||||
| #define OPENTHERM_MESSAGE_READ_MESSAGE(msg) \ | #define OPENTHERM_MESSAGE_READ_MESSAGE(msg) \ | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								esphome/components/opentherm/output/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/opentherm/output/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import output | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  | from .. import const, schema, validate, input, generate | ||||||
|  |  | ||||||
|  | DEPENDENCIES = [const.OPENTHERM] | ||||||
|  | COMPONENT_TYPE = const.OUTPUT | ||||||
|  |  | ||||||
|  | OpenthermOutput = generate.opentherm_ns.class_( | ||||||
|  |     "OpenthermOutput", output.FloatOutput, cg.Component, input.OpenthermInput | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def new_openthermoutput( | ||||||
|  |     config: dict[str, Any], key: str, _hub: cg.MockObj | ||||||
|  | ) -> cg.Pvariable: | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await output.register_output(var, config) | ||||||
|  |     cg.add(getattr(var, "set_id")(cg.RawExpression(f'"{key}_{config[CONF_ID]}"'))) | ||||||
|  |     input.generate_setters(var, config) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_entity_validation_schema(entity: schema.InputSchema) -> cv.Schema: | ||||||
|  |     return ( | ||||||
|  |         output.FLOAT_OUTPUT_SCHEMA.extend( | ||||||
|  |             {cv.GenerateID(): cv.declare_id(OpenthermOutput)} | ||||||
|  |         ) | ||||||
|  |         .extend(input.input_schema(entity)) | ||||||
|  |         .extend(cv.COMPONENT_SCHEMA) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = validate.create_component_schema( | ||||||
|  |     schema.INPUTS, get_entity_validation_schema | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config: dict[str, Any]) -> None: | ||||||
|  |     keys = await generate.component_to_code( | ||||||
|  |         COMPONENT_TYPE, schema.INPUTS, OpenthermOutput, new_openthermoutput, config | ||||||
|  |     ) | ||||||
|  |     generate.define_readers(COMPONENT_TYPE, keys) | ||||||
							
								
								
									
										18
									
								
								esphome/components/opentherm/output/output.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/opentherm/output/output.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | #include "esphome/core/helpers.h"  // for clamp() and lerp() | ||||||
|  | #include "output.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "opentherm.output"; | ||||||
|  |  | ||||||
|  | void opentherm::OpenthermOutput::write_state(float state) { | ||||||
|  |   ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); | ||||||
|  |   this->state = state < 0.003 && this->zero_means_zero_ | ||||||
|  |                     ? 0.0 | ||||||
|  |                     : clamp(lerp(state, min_value_, max_value_), min_value_, max_value_); | ||||||
|  |   this->has_state_ = true; | ||||||
|  |   ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); | ||||||
|  | } | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										33
									
								
								esphome/components/opentherm/output/output.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/opentherm/output/output.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/output/float_output.h" | ||||||
|  | #include "esphome/components/opentherm/input.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | class OpenthermOutput : public output::FloatOutput, public Component, public OpenthermInput { | ||||||
|  |  protected: | ||||||
|  |   bool has_state_ = false; | ||||||
|  |   const char *id_ = nullptr; | ||||||
|  |  | ||||||
|  |   float min_value_, max_value_; | ||||||
|  |  | ||||||
|  |  public: | ||||||
|  |   float state; | ||||||
|  |  | ||||||
|  |   void set_id(const char *id) { this->id_ = id; } | ||||||
|  |  | ||||||
|  |   void write_state(float state) override; | ||||||
|  |  | ||||||
|  |   bool has_state() { return this->has_state_; }; | ||||||
|  |  | ||||||
|  |   void set_min_value(float min_value) override { this->min_value_ = min_value; } | ||||||
|  |   void set_max_value(float max_value) override { this->max_value_ = max_value; } | ||||||
|  |   float get_min_value() { return this->min_value_; } | ||||||
|  |   float get_max_value() { return this->max_value_; } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
| @@ -11,9 +11,12 @@ from esphome.const import ( | |||||||
|     UNIT_MICROAMP, |     UNIT_MICROAMP, | ||||||
|     UNIT_PERCENT, |     UNIT_PERCENT, | ||||||
|     UNIT_REVOLUTIONS_PER_MINUTE, |     UNIT_REVOLUTIONS_PER_MINUTE, | ||||||
|  |     DEVICE_CLASS_COLD, | ||||||
|     DEVICE_CLASS_CURRENT, |     DEVICE_CLASS_CURRENT, | ||||||
|     DEVICE_CLASS_EMPTY, |     DEVICE_CLASS_EMPTY, | ||||||
|  |     DEVICE_CLASS_HEAT, | ||||||
|     DEVICE_CLASS_PRESSURE, |     DEVICE_CLASS_PRESSURE, | ||||||
|  |     DEVICE_CLASS_PROBLEM, | ||||||
|     DEVICE_CLASS_TEMPERATURE, |     DEVICE_CLASS_TEMPERATURE, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     STATE_CLASS_NONE, |     STATE_CLASS_NONE, | ||||||
| @@ -188,11 +191,23 @@ SENSORS: dict[str, SensorSchema] = { | |||||||
|         description="Boiler fan speed", |         description="Boiler fan speed", | ||||||
|         unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, |         unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, | ||||||
|         accuracy_decimals=0, |         accuracy_decimals=0, | ||||||
|  |         icon="mdi:fan", | ||||||
|         device_class=DEVICE_CLASS_EMPTY, |         device_class=DEVICE_CLASS_EMPTY, | ||||||
|         state_class=STATE_CLASS_MEASUREMENT, |         state_class=STATE_CLASS_MEASUREMENT, | ||||||
|         message="FAN_SPEED", |         message="FAN_SPEED", | ||||||
|         keep_updated=True, |         keep_updated=True, | ||||||
|         message_data="u16", |         message_data="u8_lb_60", | ||||||
|  |     ), | ||||||
|  |     "fan_speed_setpoint": SensorSchema( | ||||||
|  |         description="Boiler fan speed setpoint", | ||||||
|  |         unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, | ||||||
|  |         accuracy_decimals=0, | ||||||
|  |         icon="mdi:fan", | ||||||
|  |         device_class=DEVICE_CLASS_EMPTY, | ||||||
|  |         state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |         message="FAN_SPEED", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="u8_hb_60", | ||||||
|     ), |     ), | ||||||
|     "flame_current": SensorSchema( |     "flame_current": SensorSchema( | ||||||
|         description="Boiler flame current", |         description="Boiler flame current", | ||||||
| @@ -436,3 +451,364 @@ SENSORS: dict[str, SensorSchema] = { | |||||||
|         message_data="u8_lb", |         message_data="u8_lb", | ||||||
|     ), |     ), | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class BinarySensorSchema(EntitySchema): | ||||||
|  |     icon: Optional[str] = None | ||||||
|  |     device_class: Optional[str] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | BINARY_SENSORS: dict[str, BinarySensorSchema] = { | ||||||
|  |     "fault_indication": BinarySensorSchema( | ||||||
|  |         description="Status: Fault indication", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_0", | ||||||
|  |     ), | ||||||
|  |     "ch_active": BinarySensorSchema( | ||||||
|  |         description="Status: Central Heating active", | ||||||
|  |         device_class=DEVICE_CLASS_HEAT, | ||||||
|  |         icon="mdi:radiator", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_1", | ||||||
|  |     ), | ||||||
|  |     "dhw_active": BinarySensorSchema( | ||||||
|  |         description="Status: Domestic Hot Water active", | ||||||
|  |         device_class=DEVICE_CLASS_HEAT, | ||||||
|  |         icon="mdi:faucet", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_2", | ||||||
|  |     ), | ||||||
|  |     "flame_on": BinarySensorSchema( | ||||||
|  |         description="Status: Flame on", | ||||||
|  |         device_class=DEVICE_CLASS_HEAT, | ||||||
|  |         icon="mdi:fire", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_3", | ||||||
|  |     ), | ||||||
|  |     "cooling_active": BinarySensorSchema( | ||||||
|  |         description="Status: Cooling active", | ||||||
|  |         device_class=DEVICE_CLASS_COLD, | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_4", | ||||||
|  |     ), | ||||||
|  |     "ch2_active": BinarySensorSchema( | ||||||
|  |         description="Status: Central Heating 2 active", | ||||||
|  |         device_class=DEVICE_CLASS_HEAT, | ||||||
|  |         icon="mdi:radiator", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_5", | ||||||
|  |     ), | ||||||
|  |     "diagnostic_indication": BinarySensorSchema( | ||||||
|  |         description="Status: Diagnostic event", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_6", | ||||||
|  |     ), | ||||||
|  |     "electricity_production": BinarySensorSchema( | ||||||
|  |         description="Status: Electricity production", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_lb_7", | ||||||
|  |     ), | ||||||
|  |     "dhw_present": BinarySensorSchema( | ||||||
|  |         description="Configuration: DHW present", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_0", | ||||||
|  |     ), | ||||||
|  |     "control_type_on_off": BinarySensorSchema( | ||||||
|  |         description="Configuration: Control type is on/off", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_1", | ||||||
|  |     ), | ||||||
|  |     "cooling_supported": BinarySensorSchema( | ||||||
|  |         description="Configuration: Cooling supported", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_2", | ||||||
|  |     ), | ||||||
|  |     "dhw_storage_tank": BinarySensorSchema( | ||||||
|  |         description="Configuration: DHW storage tank", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_3", | ||||||
|  |     ), | ||||||
|  |     "controller_pump_control_allowed": BinarySensorSchema( | ||||||
|  |         description="Configuration: Controller pump control allowed", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_4", | ||||||
|  |     ), | ||||||
|  |     "ch2_present": BinarySensorSchema( | ||||||
|  |         description="Configuration: CH2 present", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_5", | ||||||
|  |     ), | ||||||
|  |     "water_filling": BinarySensorSchema( | ||||||
|  |         description="Configuration: Remote water filling", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_6", | ||||||
|  |     ), | ||||||
|  |     "heat_mode": BinarySensorSchema( | ||||||
|  |         description="Configuration: Heating or cooling", | ||||||
|  |         message="DEVICE_CONFIG", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_7", | ||||||
|  |     ), | ||||||
|  |     "dhw_setpoint_transfer_enabled": BinarySensorSchema( | ||||||
|  |         description="Remote boiler parameters: DHW setpoint transfer enabled", | ||||||
|  |         message="REMOTE", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_0", | ||||||
|  |     ), | ||||||
|  |     "max_ch_setpoint_transfer_enabled": BinarySensorSchema( | ||||||
|  |         description="Remote boiler parameters: CH maximum setpoint transfer enabled", | ||||||
|  |         message="REMOTE", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_hb_1", | ||||||
|  |     ), | ||||||
|  |     "dhw_setpoint_rw": BinarySensorSchema( | ||||||
|  |         description="Remote boiler parameters: DHW setpoint read/write", | ||||||
|  |         message="REMOTE", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_lb_0", | ||||||
|  |     ), | ||||||
|  |     "max_ch_setpoint_rw": BinarySensorSchema( | ||||||
|  |         description="Remote boiler parameters: CH maximum setpoint read/write", | ||||||
|  |         message="REMOTE", | ||||||
|  |         keep_updated=False, | ||||||
|  |         message_data="flag8_lb_1", | ||||||
|  |     ), | ||||||
|  |     "service_request": BinarySensorSchema( | ||||||
|  |         description="Service required", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="FAULT_FLAGS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_0", | ||||||
|  |     ), | ||||||
|  |     "lockout_reset": BinarySensorSchema( | ||||||
|  |         description="Lockout Reset", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="FAULT_FLAGS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_1", | ||||||
|  |     ), | ||||||
|  |     "low_water_pressure": BinarySensorSchema( | ||||||
|  |         description="Low water pressure fault", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="FAULT_FLAGS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_2", | ||||||
|  |     ), | ||||||
|  |     "flame_fault": BinarySensorSchema( | ||||||
|  |         description="Flame fault", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="FAULT_FLAGS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_3", | ||||||
|  |     ), | ||||||
|  |     "air_pressure_fault": BinarySensorSchema( | ||||||
|  |         description="Air pressure fault", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="FAULT_FLAGS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_4", | ||||||
|  |     ), | ||||||
|  |     "water_over_temp": BinarySensorSchema( | ||||||
|  |         description="Water overtemperature", | ||||||
|  |         device_class=DEVICE_CLASS_PROBLEM, | ||||||
|  |         message="FAULT_FLAGS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_5", | ||||||
|  |     ), | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class SwitchSchema(EntitySchema): | ||||||
|  |     default_mode: Optional[str] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SWITCHES: dict[str, SwitchSchema] = { | ||||||
|  |     "ch_enable": SwitchSchema( | ||||||
|  |         description="Central Heating enabled", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_0", | ||||||
|  |         default_mode="restore_default_off", | ||||||
|  |     ), | ||||||
|  |     "dhw_enable": SwitchSchema( | ||||||
|  |         description="Domestic Hot Water enabled", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_1", | ||||||
|  |         default_mode="restore_default_off", | ||||||
|  |     ), | ||||||
|  |     "cooling_enable": SwitchSchema( | ||||||
|  |         description="Cooling enabled", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_2", | ||||||
|  |         default_mode="restore_default_off", | ||||||
|  |     ), | ||||||
|  |     "otc_active": SwitchSchema( | ||||||
|  |         description="Outside temperature compensation active", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_3", | ||||||
|  |         default_mode="restore_default_off", | ||||||
|  |     ), | ||||||
|  |     "ch2_active": SwitchSchema( | ||||||
|  |         description="Central Heating 2 active", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_4", | ||||||
|  |         default_mode="restore_default_off", | ||||||
|  |     ), | ||||||
|  |     "summer_mode_active": SwitchSchema( | ||||||
|  |         description="Summer mode active", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_5", | ||||||
|  |         default_mode="restore_default_off", | ||||||
|  |     ), | ||||||
|  |     "dhw_block": SwitchSchema( | ||||||
|  |         description="DHW blocked", | ||||||
|  |         message="STATUS", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="flag8_hb_6", | ||||||
|  |         default_mode="restore_default_off", | ||||||
|  |     ), | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class AutoConfigure: | ||||||
|  |     message: str | ||||||
|  |     message_data: str | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class InputSchema(EntitySchema): | ||||||
|  |     unit_of_measurement: str | ||||||
|  |     step: float | ||||||
|  |     range: tuple[int, int] | ||||||
|  |     icon: Optional[str] = None | ||||||
|  |     auto_max_value: Optional[AutoConfigure] = None | ||||||
|  |     auto_min_value: Optional[AutoConfigure] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | INPUTS: dict[str, InputSchema] = { | ||||||
|  |     "t_set": InputSchema( | ||||||
|  |         description="Control setpoint: temperature setpoint for the boiler's supply water", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="CH_SETPOINT", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(0, 100), | ||||||
|  |         auto_max_value=AutoConfigure(message="MAX_CH_SETPOINT", message_data="f88"), | ||||||
|  |     ), | ||||||
|  |     "t_set_ch2": InputSchema( | ||||||
|  |         description="Control setpoint 2: temperature setpoint for the boiler's supply water on the second heating circuit", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="CH2_SETPOINT", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(0, 100), | ||||||
|  |         auto_max_value=AutoConfigure(message="MAX_CH_SETPOINT", message_data="f88"), | ||||||
|  |     ), | ||||||
|  |     "cooling_control": InputSchema( | ||||||
|  |         description="Cooling control signal", | ||||||
|  |         unit_of_measurement=UNIT_PERCENT, | ||||||
|  |         step=1.0, | ||||||
|  |         message="COOLING_CONTROL", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(0, 100), | ||||||
|  |     ), | ||||||
|  |     "t_dhw_set": InputSchema( | ||||||
|  |         description="Domestic hot water temperature setpoint", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="DHW_SETPOINT", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(0, 127), | ||||||
|  |         auto_min_value=AutoConfigure(message="DHW_BOUNDS", message_data="s8_lb"), | ||||||
|  |         auto_max_value=AutoConfigure(message="DHW_BOUNDS", message_data="s8_hb"), | ||||||
|  |     ), | ||||||
|  |     "max_t_set": InputSchema( | ||||||
|  |         description="Maximum allowable CH water setpoint", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="MAX_CH_SETPOINT", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(0, 127), | ||||||
|  |         auto_min_value=AutoConfigure(message="CH_BOUNDS", message_data="s8_lb"), | ||||||
|  |         auto_max_value=AutoConfigure(message="CH_BOUNDS", message_data="s8_hb"), | ||||||
|  |     ), | ||||||
|  |     "t_room_set": InputSchema( | ||||||
|  |         description="Current room temperature setpoint (informational)", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="ROOM_SETPOINT", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(-40, 127), | ||||||
|  |     ), | ||||||
|  |     "t_room_set_ch2": InputSchema( | ||||||
|  |         description="Current room temperature setpoint on CH2 (informational)", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="ROOM_SETPOINT_CH2", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(-40, 127), | ||||||
|  |     ), | ||||||
|  |     "t_room": InputSchema( | ||||||
|  |         description="Current sensed room temperature (informational)", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="ROOM_TEMP", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(-40, 127), | ||||||
|  |     ), | ||||||
|  |     "max_rel_mod_level": InputSchema( | ||||||
|  |         description="Maximum relative modulation level", | ||||||
|  |         unit_of_measurement=UNIT_PERCENT, | ||||||
|  |         step=1, | ||||||
|  |         icon="mdi:percent", | ||||||
|  |         message="MAX_MODULATION_LEVEL", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(0, 100), | ||||||
|  |     ), | ||||||
|  |     "otc_hc_ratio": InputSchema( | ||||||
|  |         description="OTC heat curve ratio", | ||||||
|  |         unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |         step=0.1, | ||||||
|  |         message="OTC_CURVE_RATIO", | ||||||
|  |         keep_updated=True, | ||||||
|  |         message_data="f88", | ||||||
|  |         range=(0, 127), | ||||||
|  |         auto_min_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_lb"), | ||||||
|  |         auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"), | ||||||
|  |     ), | ||||||
|  | } | ||||||
|   | |||||||
| @@ -7,6 +7,18 @@ from .. import const, schema, validate, generate | |||||||
| DEPENDENCIES = [const.OPENTHERM] | DEPENDENCIES = [const.OPENTHERM] | ||||||
| COMPONENT_TYPE = const.SENSOR | COMPONENT_TYPE = const.SENSOR | ||||||
|  |  | ||||||
|  | MSG_DATA_TYPES = { | ||||||
|  |     "u8_lb", | ||||||
|  |     "u8_hb", | ||||||
|  |     "s8_lb", | ||||||
|  |     "s8_hb", | ||||||
|  |     "u8_lb_60", | ||||||
|  |     "u8_hb_60", | ||||||
|  |     "u16", | ||||||
|  |     "s16", | ||||||
|  |     "f88", | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: | def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: | ||||||
|     return sensor.sensor_schema( |     return sensor.sensor_schema( | ||||||
| @@ -17,6 +29,10 @@ def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: | |||||||
|         or sensor._UNDEF,  # pylint: disable=protected-access |         or sensor._UNDEF,  # pylint: disable=protected-access | ||||||
|         icon=entity.icon or sensor._UNDEF,  # pylint: disable=protected-access |         icon=entity.icon or sensor._UNDEF,  # pylint: disable=protected-access | ||||||
|         state_class=entity.state_class, |         state_class=entity.state_class, | ||||||
|  |     ).extend( | ||||||
|  |         { | ||||||
|  |             cv.Optional(const.CONF_DATA_TYPE): cv.one_of(*MSG_DATA_TYPES), | ||||||
|  |         } | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								esphome/components/opentherm/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/opentherm/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import switch | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  | from .. import const, schema, validate, generate | ||||||
|  |  | ||||||
|  | DEPENDENCIES = [const.OPENTHERM] | ||||||
|  | COMPONENT_TYPE = const.SWITCH | ||||||
|  |  | ||||||
|  | OpenthermSwitch = generate.opentherm_ns.class_( | ||||||
|  |     "OpenthermSwitch", switch.Switch, cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def new_openthermswitch(config: dict[str, Any]) -> cg.Pvariable: | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await switch.register_switch(var, config) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_entity_validation_schema(entity: schema.SwitchSchema) -> cv.Schema: | ||||||
|  |     return switch.SWITCH_SCHEMA.extend( | ||||||
|  |         {cv.GenerateID(): cv.declare_id(OpenthermSwitch)} | ||||||
|  |     ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = validate.create_component_schema( | ||||||
|  |     schema.SWITCHES, get_entity_validation_schema | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config: dict[str, Any]) -> None: | ||||||
|  |     keys = await generate.component_to_code( | ||||||
|  |         COMPONENT_TYPE, | ||||||
|  |         schema.SWITCHES, | ||||||
|  |         OpenthermSwitch, | ||||||
|  |         generate.create_only_conf(new_openthermswitch), | ||||||
|  |         config, | ||||||
|  |     ) | ||||||
|  |     generate.define_readers(COMPONENT_TYPE, keys) | ||||||
							
								
								
									
										28
									
								
								esphome/components/opentherm/switch/switch.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								esphome/components/opentherm/switch/switch.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | #include "switch.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "opentherm.switch"; | ||||||
|  |  | ||||||
|  | void OpenthermSwitch::write_state(bool state) { this->publish_state(state); } | ||||||
|  |  | ||||||
|  | void OpenthermSwitch::setup() { | ||||||
|  |   auto restored = this->get_initial_state_with_restore_mode(); | ||||||
|  |   bool state = false; | ||||||
|  |   if (!restored.has_value()) { | ||||||
|  |     ESP_LOGD(TAG, "Couldn't restore state for OpenTherm switch '%s'", this->get_name().c_str()); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGD(TAG, "Restored state for OpenTherm switch '%s': %d", this->get_name().c_str(), restored.value()); | ||||||
|  |     state = restored.value(); | ||||||
|  |   } | ||||||
|  |   this->write_state(state); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void OpenthermSwitch::dump_config() { | ||||||
|  |   LOG_SWITCH("", "OpenTherm Switch", this); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Current state: %d", this->state); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										20
									
								
								esphome/components/opentherm/switch/switch.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/opentherm/switch/switch.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/switch/switch.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace opentherm { | ||||||
|  |  | ||||||
|  | class OpenthermSwitch : public switch_::Switch, public Component { | ||||||
|  |  protected: | ||||||
|  |   void write_state(bool state) override; | ||||||
|  |  | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace opentherm | ||||||
|  | }  // namespace esphome | ||||||
| @@ -12,10 +12,41 @@ opentherm: | |||||||
|   cooling_enable: false |   cooling_enable: false | ||||||
|   otc_active: false |   otc_active: false | ||||||
|   ch2_active: true |   ch2_active: true | ||||||
|  |   t_room: boiler_sensor | ||||||
|   summer_mode_active: true |   summer_mode_active: true | ||||||
|   dhw_block: true |   dhw_block: true | ||||||
|   sync_mode: true |   sync_mode: true | ||||||
|  |  | ||||||
|  | output: | ||||||
|  |   - platform: opentherm | ||||||
|  |     t_set: | ||||||
|  |       id: t_set | ||||||
|  |       min_value: 20 | ||||||
|  |       auto_max_value: true | ||||||
|  |       zero_means_zero: true | ||||||
|  |     t_set_ch2: | ||||||
|  |       id: t_set_ch2 | ||||||
|  |       min_value: 20 | ||||||
|  |       max_value: 40 | ||||||
|  |       zero_means_zero: true | ||||||
|  |  | ||||||
|  | number: | ||||||
|  |   - platform: opentherm | ||||||
|  |     cooling_control: | ||||||
|  |       name: "Boiler Cooling control signal" | ||||||
|  |     t_dhw_set: | ||||||
|  |       name: "Boiler DHW Setpoint" | ||||||
|  |     max_t_set: | ||||||
|  |       name: "Boiler Max Setpoint" | ||||||
|  |     t_room_set: | ||||||
|  |       name: "Boiler Room Setpoint" | ||||||
|  |     t_room_set_ch2: | ||||||
|  |       name: "Boiler Room Setpoint CH2" | ||||||
|  |     max_rel_mod_level: | ||||||
|  |       name: "Maximum relative modulation level" | ||||||
|  |     otc_hc_ratio: | ||||||
|  |       name: "OTC heat curve ratio" | ||||||
|  |  | ||||||
| sensor: | sensor: | ||||||
|   - platform: opentherm |   - platform: opentherm | ||||||
|     rel_mod_level: |     rel_mod_level: | ||||||
| @@ -25,6 +56,7 @@ sensor: | |||||||
|     dhw_flow_rate: |     dhw_flow_rate: | ||||||
|       name: "Boiler Water flow rate in DHW circuit" |       name: "Boiler Water flow rate in DHW circuit" | ||||||
|     t_boiler: |     t_boiler: | ||||||
|  |       id: "boiler_sensor" | ||||||
|       name: "Boiler water temperature" |       name: "Boiler water temperature" | ||||||
|     t_dhw: |     t_dhw: | ||||||
|       name: "Boiler DHW temperature" |       name: "Boiler DHW temperature" | ||||||
| @@ -74,3 +106,55 @@ sensor: | |||||||
|       name: "OTC heat curve ratio upper bound" |       name: "OTC heat curve ratio upper bound" | ||||||
|     otc_hc_ratio_lb: |     otc_hc_ratio_lb: | ||||||
|       name: "OTC heat curve ratio lower bound" |       name: "OTC heat curve ratio lower bound" | ||||||
|  |  | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: opentherm | ||||||
|  |     fault_indication: | ||||||
|  |       name: "Boiler Fault indication" | ||||||
|  |     ch_active: | ||||||
|  |       name: "Boiler Central Heating active" | ||||||
|  |     dhw_active: | ||||||
|  |       name: "Boiler Domestic Hot Water active" | ||||||
|  |     flame_on: | ||||||
|  |       name: "Boiler Flame on" | ||||||
|  |     cooling_active: | ||||||
|  |       name: "Boiler Cooling active" | ||||||
|  |     ch2_active: | ||||||
|  |       name: "Boiler Central Heating 2 active" | ||||||
|  |     diagnostic_indication: | ||||||
|  |       name: "Boiler Diagnostic event" | ||||||
|  |     dhw_present: | ||||||
|  |       name: "Boiler DHW present" | ||||||
|  |     control_type_on_off: | ||||||
|  |       name: "Boiler Control type is on/off" | ||||||
|  |     cooling_supported: | ||||||
|  |       name: "Boiler Cooling supported" | ||||||
|  |     dhw_storage_tank: | ||||||
|  |       name: "Boiler DHW storage tank" | ||||||
|  |     controller_pump_control_allowed: | ||||||
|  |       name: "Boiler Controller pump control allowed" | ||||||
|  |     ch2_present: | ||||||
|  |       name: "Boiler CH2 present" | ||||||
|  |     dhw_setpoint_transfer_enabled: | ||||||
|  |       name: "Boiler DHW setpoint transfer enabled" | ||||||
|  |     max_ch_setpoint_transfer_enabled: | ||||||
|  |       name: "Boiler CH maximum setpoint transfer enabled" | ||||||
|  |     dhw_setpoint_rw: | ||||||
|  |       name: "Boiler DHW setpoint read/write" | ||||||
|  |     max_ch_setpoint_rw: | ||||||
|  |       name: "Boiler CH maximum setpoint read/write" | ||||||
|  |  | ||||||
|  | switch: | ||||||
|  |   - platform: opentherm | ||||||
|  |     ch_enable: | ||||||
|  |       name: "Boiler Central Heating enabled" | ||||||
|  |       restore_mode: RESTORE_DEFAULT_ON | ||||||
|  |     dhw_enable: | ||||||
|  |       name: "Boiler Domestic Hot Water enabled" | ||||||
|  |     cooling_enable: | ||||||
|  |       name: "Boiler Cooling enabled" | ||||||
|  |       restore_mode: ALWAYS_OFF | ||||||
|  |     otc_active: | ||||||
|  |       name: "Boiler Outside temperature compensation active" | ||||||
|  |     ch2_active: | ||||||
|  |       name: "Boiler Central Heating 2 active" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user