[opentherm] Message ordering, on-the-fly message editing, code improvements (#7903)

This commit is contained in:
Oleg Tarasov 2024-12-16 02:04:26 +03:00 committed by GitHub
parent 9816c27031
commit a6957b9d3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 475 additions and 145 deletions

View file

@ -1,10 +1,12 @@
from typing import Any from typing import Any
import logging
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import pins from esphome import pins
from esphome.components import sensor from esphome.components import sensor
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266, CONF_TRIGGER_ID
from . import const, schema, validate, generate from . import const, schema, validate, generate
CODEOWNERS = ["@olegtarasov"] CODEOWNERS = ["@olegtarasov"]
@ -20,7 +22,21 @@ CONF_CH2_ACTIVE = "ch2_active"
CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active"
CONF_DHW_BLOCK = "dhw_block" CONF_DHW_BLOCK = "dhw_block"
CONF_SYNC_MODE = "sync_mode" CONF_SYNC_MODE = "sync_mode"
CONF_OPENTHERM_VERSION = "opentherm_version" CONF_OPENTHERM_VERSION = "opentherm_version" # Deprecated, will be removed
CONF_BEFORE_SEND = "before_send"
CONF_BEFORE_PROCESS_RESPONSE = "before_process_response"
# Triggers
BeforeSendTrigger = generate.opentherm_ns.class_(
"BeforeSendTrigger",
automation.Trigger.template(generate.OpenthermData.operator("ref")),
)
BeforeProcessResponseTrigger = generate.opentherm_ns.class_(
"BeforeProcessResponseTrigger",
automation.Trigger.template(generate.OpenthermData.operator("ref")),
)
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
@ -36,7 +52,19 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
cv.Optional(CONF_SYNC_MODE, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, # Deprecated
cv.Optional(CONF_BEFORE_SEND): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger),
}
),
cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BeforeProcessResponseTrigger
),
}
),
} }
) )
.extend( .extend(
@ -44,6 +72,11 @@ CONFIG_SCHEMA = cv.All(
schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor))
) )
) )
.extend(
validate.create_entities_schema(
schema.SETTINGS, (lambda s: s.validation_schema)
)
)
.extend(cv.COMPONENT_SCHEMA), .extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
) )
@ -60,18 +93,33 @@ async def to_code(config: dict[str, Any]) -> None:
out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN]) out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN])
cg.add(var.set_out_pin(out_pin)) cg.add(var.set_out_pin(out_pin))
non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} non_sensors = {
CONF_ID,
CONF_IN_PIN,
CONF_OUT_PIN,
CONF_BEFORE_SEND,
CONF_BEFORE_PROCESS_RESPONSE,
}
input_sensors = [] input_sensors = []
settings = []
for key, value in config.items(): for key, value in config.items():
if key in non_sensors: if key in non_sensors:
continue continue
if key in schema.INPUTS: if key in schema.INPUTS:
input_sensor = await cg.get_variable(value) input_sensor = await cg.get_variable(value)
cg.add( cg.add(getattr(var, f"set_{key}_{const.INPUT_SENSOR}")(input_sensor))
getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor)
)
input_sensors.append(key) input_sensors.append(key)
elif key in schema.SETTINGS:
if value == schema.SETTINGS[key].default_value:
continue
cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value))
settings.append(key)
else: else:
if key == CONF_OPENTHERM_VERSION:
_LOGGER.warning(
"opentherm_version is deprecated and will be removed in esphome 2025.2.0\n"
"Please change to 'opentherm_version_controller'."
)
cg.add(getattr(var, f"set_{key}")(value)) cg.add(getattr(var, f"set_{key}")(value))
if len(input_sensors) > 0: if len(input_sensors) > 0:
@ -81,3 +129,21 @@ async def to_code(config: dict[str, Any]) -> None:
) )
generate.define_readers(const.INPUT_SENSOR, input_sensors) generate.define_readers(const.INPUT_SENSOR, input_sensors)
generate.add_messages(var, input_sensors, schema.INPUTS) generate.add_messages(var, input_sensors, schema.INPUTS)
if len(settings) > 0:
generate.define_has_settings(settings, schema.SETTINGS)
generate.define_message_handler(const.SETTING, settings, schema.SETTINGS)
generate.define_setting_readers(const.SETTING, settings)
generate.add_messages(var, settings, schema.SETTINGS)
for conf in config.get(CONF_BEFORE_SEND, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
)
for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
)

View 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

View file

@ -9,3 +9,4 @@ SWITCH = "switch"
NUMBER = "number" NUMBER = "number"
OUTPUT = "output" OUTPUT = "output"
INPUT_SENSOR = "input_sensor" INPUT_SENSOR = "input_sensor"
SETTING = "setting"

View file

@ -1,13 +1,14 @@
from collections.abc import Awaitable from collections.abc import Awaitable
from typing import Any, Callable from typing import Any, Callable, Optional
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import CONF_ID from esphome.const import CONF_ID
from . import const from . import const
from .schema import TSchema from .schema import TSchema, SettingSchema
opentherm_ns = cg.esphome_ns.namespace("opentherm") opentherm_ns = cg.esphome_ns.namespace("opentherm")
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
OpenthermData = opentherm_ns.class_("OpenthermData")
def define_has_component(component_type: str, keys: list[str]) -> None: def define_has_component(component_type: str, keys: list[str]) -> None:
@ -21,6 +22,24 @@ def define_has_component(component_type: str, keys: list[str]) -> None:
cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}") cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}")
# We need a separate set of macros for settings because there are different backing field types we need to take
# into account
def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> None:
cg.add_define(
"OPENTHERM_SETTING_LIST(F, sep)",
cg.RawExpression(
" sep ".join(
map(
lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})",
keys,
)
)
),
)
for key in keys:
cg.add_define(f"OPENTHERM_HAS_SETTING_{key}")
def define_message_handler( def define_message_handler(
component_type: str, keys: list[str], schemas: dict[str, TSchema] component_type: str, keys: list[str], schemas: dict[str, TSchema]
) -> None: ) -> None:
@ -74,16 +93,30 @@ def define_readers(component_type: str, keys: list[str]) -> None:
) )
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): def define_setting_readers(component_type: str, keys: list[str]) -> None:
messages: set[tuple[str, bool]] = set()
for key in keys: for key in keys:
messages.add((schemas[key].message, schemas[key].keep_updated)) cg.add_define(
for msg, keep_updated in messages: f"OPENTHERM_READ_{key}",
cg.RawExpression(f"this->{key}_{component_type.lower()}"),
)
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
messages: dict[str, tuple[bool, Optional[int]]] = {}
for key in keys:
messages[schemas[key].message] = (
schemas[key].keep_updated,
schemas[key].order if hasattr(schemas[key], "order") else None,
)
for msg, (keep_updated, order) in messages.items():
msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}")
if keep_updated: if keep_updated:
cg.add(hub.add_repeating_message(msg_expr)) cg.add(hub.add_repeating_message(msg_expr))
else: else:
cg.add(hub.add_initial_message(msg_expr)) if order is not None:
cg.add(hub.add_initial_message(msg_expr, order))
else:
cg.add(hub.add_initial_message(msg_expr))
def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None: def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None:

View file

@ -63,7 +63,7 @@ void write_f88(const float value, OpenthermData &data) { data.f88(value); }
OpenthermData OpenthermHub::build_request_(MessageId request_id) const { OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
OpenthermData data; OpenthermData data;
data.type = 0; data.type = 0;
data.id = 0; data.id = request_id;
data.valueHB = 0; data.valueHB = 0;
data.valueLB = 0; data.valueLB = 0;
@ -82,28 +82,13 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
// NOLINTEND // NOLINTEND
data.type = MessageType::READ_DATA; data.type = MessageType::READ_DATA;
data.id = MessageId::STATUS;
data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) | data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) |
(summer_mode_is_active << 5) | (dhw_blocked << 6); (summer_mode_is_active << 5) | (dhw_blocked << 6);
return data; return data;
} }
// Another special case is OpenTherm version number which is configured at hub level as a constant // Next, we start with write requests from switches and other inputs,
if (request_id == MessageId::OT_VERSION_CONTROLLER) {
data.type = MessageType::WRITE_DATA;
data.id = MessageId::OT_VERSION_CONTROLLER;
data.f88(this->opentherm_version_);
return data;
}
// Disable incomplete switch statement warnings, because the cases in each
// switch are generated based on the configured sensors and inputs.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
// Next, we start with the write requests from switches and other inputs,
// because we would want to write that data if it is available, rather than // because we would want to write that data if it is available, rather than
// request a read for that type (in the case that both read and write are // request a read for that type (in the case that both read and write are
// supported). // supported).
@ -116,14 +101,23 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, ,
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
OPENTHERM_SETTING_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_SETTING, ,
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
default:
break;
} }
// Finally, handle the simple read requests, which only change with the message id. // Finally, handle the simple read requests, which only change with the message id.
switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } switch (request_id) {
OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , )
default:
break;
}
switch (request_id) { switch (request_id) {
OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , )
default:
break;
} }
#pragma GCC diagnostic pop
// And if we get here, a message was requested which somehow wasn't handled. // And if we get here, a message was requested which somehow wasn't handled.
// This shouldn't happen due to the way the defines are configured, so we // This shouldn't happen due to the way the defines are configured, so we
@ -163,19 +157,37 @@ void OpenthermHub::setup() {
// communicate at least once every second. Sending the status request is // communicate at least once every second. Sending the status request is
// good practice anyway. // good practice anyway.
this->add_repeating_message(MessageId::STATUS); this->add_repeating_message(MessageId::STATUS);
this->write_initial_messages_(this->messages_);
// Also ensure that we start communication with the STATUS message this->message_iterator_ = this->messages_.begin();
this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::STATUS);
if (this->opentherm_version_ > 0.0f) {
this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::OT_VERSION_CONTROLLER);
}
this->current_message_iterator_ = this->initial_messages_.begin();
} }
void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } void OpenthermHub::on_shutdown() { this->opentherm_->stop(); }
// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?)
void OpenthermHub::write_initial_messages_(std::vector<MessageId> &target) { // NOLINT
std::vector<std::pair<MessageId, uint8_t>> sorted;
std::copy_if(this->configured_messages_.begin(), this->configured_messages_.end(), std::back_inserter(sorted),
[](const std::pair<MessageId, uint8_t> &pair) { return pair.second < REPEATING_MESSAGE_ORDER; });
std::sort(sorted.begin(), sorted.end(),
[](const std::pair<MessageId, uint8_t> &a, const std::pair<MessageId, uint8_t> &b) {
return a.second < b.second;
});
target.clear();
std::transform(sorted.begin(), sorted.end(), std::back_inserter(target),
[](const std::pair<MessageId, uint8_t> &pair) { return pair.first; });
}
// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?)
void OpenthermHub::write_repeating_messages_(std::vector<MessageId> &target) { // NOLINT
target.clear();
for (auto const &pair : this->configured_messages_) {
if (pair.second == REPEATING_MESSAGE_ORDER) {
target.push_back(pair.first);
}
}
}
void OpenthermHub::loop() { void OpenthermHub::loop() {
if (this->sync_mode_) { if (this->sync_mode_) {
this->sync_loop_(); this->sync_loop_();
@ -184,29 +196,18 @@ void OpenthermHub::loop() {
auto cur_time = millis(); auto cur_time = millis();
auto const cur_mode = this->opentherm_->get_mode(); auto const cur_mode = this->opentherm_->get_mode();
if (this->handle_error_(cur_mode)) {
return;
}
switch (cur_mode) { switch (cur_mode) {
case OperationMode::WRITE: case OperationMode::WRITE:
case OperationMode::READ: case OperationMode::READ:
case OperationMode::LISTEN: case OperationMode::LISTEN:
if (!this->check_timings_(cur_time)) {
break;
}
this->last_mode_ = cur_mode;
break;
case OperationMode::ERROR_PROTOCOL:
if (this->last_mode_ == OperationMode::WRITE) {
this->handle_protocol_write_error_();
} else if (this->last_mode_ == OperationMode::READ) {
this->handle_protocol_read_error_();
}
this->stop_opentherm_();
break;
case OperationMode::ERROR_TIMEOUT:
this->handle_timeout_error_();
this->stop_opentherm_();
break; break;
case OperationMode::IDLE: case OperationMode::IDLE:
this->check_timings_(cur_time);
if (this->should_skip_loop_(cur_time)) { if (this->should_skip_loop_(cur_time)) {
break; break;
} }
@ -219,6 +220,28 @@ void OpenthermHub::loop() {
case OperationMode::RECEIVED: case OperationMode::RECEIVED:
this->read_response_(); this->read_response_();
break; break;
default:
break;
}
this->last_mode_ = cur_mode;
}
bool OpenthermHub::handle_error_(OperationMode mode) {
switch (mode) {
case OperationMode::ERROR_PROTOCOL:
// Protocol error can happen only while reading boiler response.
this->handle_protocol_error_();
return true;
case OperationMode::ERROR_TIMEOUT:
// Timeout error might happen while we wait for device to respond.
this->handle_timeout_error_();
return true;
case OperationMode::ERROR_TIMER:
// Timer error can happen only on ESP32.
this->handle_timer_error_();
return true;
default:
return false;
} }
} }
@ -237,16 +260,20 @@ void OpenthermHub::sync_loop_() {
} }
this->start_conversation_(); this->start_conversation_();
// There may be a timer error at this point
if (this->handle_error_(this->opentherm_->get_mode())) {
return;
}
// Spin while message is being sent to device
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during send"); ESP_LOGE(TAG, "Hub timeout triggered during send");
this->stop_opentherm_(); this->stop_opentherm_();
return; return;
} }
if (this->opentherm_->is_error()) { // Check for errors and ensure we are in the right state (message sent successfully)
this->handle_protocol_write_error_(); if (this->handle_error_(this->opentherm_->get_mode())) {
this->stop_opentherm_();
return; return;
} else if (!this->opentherm_->is_sent()) { } else if (!this->opentherm_->is_sent()) {
ESP_LOGW(TAG, "Unexpected state after sending request: %s", ESP_LOGW(TAG, "Unexpected state after sending request: %s",
@ -257,19 +284,20 @@ void OpenthermHub::sync_loop_() {
// Listen for the response // Listen for the response
this->opentherm_->listen(); this->opentherm_->listen();
// There may be a timer error at this point
if (this->handle_error_(this->opentherm_->get_mode())) {
return;
}
// Spin while response is being received
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during receive"); ESP_LOGE(TAG, "Hub timeout triggered during receive");
this->stop_opentherm_(); this->stop_opentherm_();
return; return;
} }
if (this->opentherm_->is_timeout()) { // Check for errors and ensure we are in the right state (message received successfully)
this->handle_timeout_error_(); if (this->handle_error_(this->opentherm_->get_mode())) {
this->stop_opentherm_();
return;
} else if (this->opentherm_->is_protocol_error()) {
this->handle_protocol_read_error_();
this->stop_opentherm_();
return; return;
} else if (!this->opentherm_->has_message()) { } else if (!this->opentherm_->has_message()) {
ESP_LOGW(TAG, "Unexpected state after receiving response: %s", ESP_LOGW(TAG, "Unexpected state after receiving response: %s",
@ -281,17 +309,13 @@ void OpenthermHub::sync_loop_() {
this->read_response_(); this->read_response_();
} }
bool OpenthermHub::check_timings_(uint32_t cur_time) { void OpenthermHub::check_timings_(uint32_t cur_time) {
if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) { if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) {
ESP_LOGW(TAG, ESP_LOGW(TAG,
"%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " "%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other "
"components that might slow the loop down.", "components that might slow the loop down.",
(int) (cur_time - this->last_conversation_start_)); (int) (cur_time - this->last_conversation_start_));
this->stop_opentherm_();
return false;
} }
return true;
} }
bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
@ -304,14 +328,17 @@ bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
} }
void OpenthermHub::start_conversation_() { void OpenthermHub::start_conversation_() {
if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { if (this->message_iterator_ == this->messages_.end()) {
this->sending_initial_ = false; if (this->sending_initial_) {
this->current_message_iterator_ = this->repeating_messages_.begin(); this->sending_initial_ = false;
} else if (this->current_message_iterator_ == this->repeating_messages_.end()) { this->write_repeating_messages_(this->messages_);
this->current_message_iterator_ = this->repeating_messages_.begin(); }
this->message_iterator_ = this->messages_.begin();
} }
auto request = this->build_request_(*this->current_message_iterator_); auto request = this->build_request_(*this->message_iterator_);
this->before_send_callback_.call(request);
ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id, ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
this->opentherm_->message_id_to_str((MessageId) request.id)); this->opentherm_->message_id_to_str((MessageId) request.id));
@ -331,37 +358,48 @@ void OpenthermHub::read_response_() {
this->stop_opentherm_(); this->stop_opentherm_();
this->before_process_response_callback_.call(response);
this->process_response(response); this->process_response(response);
this->current_message_iterator_++; this->message_iterator_++;
} }
void OpenthermHub::stop_opentherm_() { void OpenthermHub::stop_opentherm_() {
this->opentherm_->stop(); this->opentherm_->stop();
this->last_conversation_end_ = millis(); this->last_conversation_end_ = millis();
} }
void OpenthermHub::handle_protocol_write_error_() {
ESP_LOGW(TAG, "Error while sending request: %s", void OpenthermHub::handle_protocol_error_() {
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
this->opentherm_->debug_data(this->last_request_);
}
void OpenthermHub::handle_protocol_read_error_() {
OpenThermError error; OpenThermError error;
this->opentherm_->get_protocol_error(error); this->opentherm_->get_protocol_error(error);
ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", ESP_LOGW(TAG, "Protocol error occured while receiving response: %s",
this->opentherm_->protocol_error_to_to_str(error.error_type)); this->opentherm_->protocol_error_to_str(error.error_type));
this->opentherm_->debug_error(error); this->opentherm_->debug_error(error);
}
void OpenthermHub::handle_timeout_error_() {
ESP_LOGW(TAG, "Receive response timed out at a protocol level");
this->stop_opentherm_(); this->stop_opentherm_();
} }
void OpenthermHub::handle_timeout_error_() {
ESP_LOGW(TAG, "Timeout while waiting for response from device");
this->stop_opentherm_();
}
void OpenthermHub::handle_timer_error_() {
this->opentherm_->report_and_reset_timer_error();
this->stop_opentherm_();
// Timer error is critical, there is no point in retrying.
this->mark_failed();
}
void OpenthermHub::dump_config() { void OpenthermHub::dump_config() {
std::vector<MessageId> initial_messages;
std::vector<MessageId> repeating_messages;
this->write_initial_messages_(initial_messages);
this->write_repeating_messages_(repeating_messages);
ESP_LOGCONFIG(TAG, "OpenTherm:"); ESP_LOGCONFIG(TAG, "OpenTherm:");
LOG_PIN(" In: ", this->in_pin_); LOG_PIN(" In: ", this->in_pin_);
LOG_PIN(" Out: ", this->out_pin_); LOG_PIN(" Out: ", this->out_pin_);
ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_); ESP_LOGCONFIG(TAG, " Sync mode: %s", YESNO(this->sync_mode_));
ESP_LOGCONFIG(TAG, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, )));
@ -369,12 +407,12 @@ void OpenthermHub::dump_config() {
ESP_LOGCONFIG(TAG, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Initial requests:"); ESP_LOGCONFIG(TAG, " Initial requests:");
for (auto type : this->initial_messages_) { for (auto type : initial_messages) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type));
} }
ESP_LOGCONFIG(TAG, " Repeating requests:"); ESP_LOGCONFIG(TAG, " Repeating requests:");
for (auto type : this->repeating_messages_) { for (auto type : repeating_messages) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type));
} }
} }

View file

@ -38,6 +38,9 @@
namespace esphome { namespace esphome {
namespace opentherm { namespace opentherm {
static const uint8_t REPEATING_MESSAGE_ORDER = 255;
static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254;
// OpenTherm component for ESPHome // OpenTherm component for ESPHome
class OpenthermHub : public Component { class OpenthermHub : public Component {
protected: protected:
@ -58,15 +61,12 @@ class OpenthermHub : public Component {
OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, )
// The set of initial messages to send on starting communication with the boiler OPENTHERM_SETTING_LIST(OPENTHERM_DECLARE_SETTING, )
std::vector<MessageId> initial_messages_;
// and the repeating messages which are sent repeatedly to update various sensors
// and boiler parameters (like the setpoint).
std::vector<MessageId> repeating_messages_;
// Indicates if we are still working on the initial requests or not
bool sending_initial_ = true; bool sending_initial_ = true;
// Index for the current request in one of the _requests sets. std::unordered_map<MessageId, uint8_t> configured_messages_;
std::vector<MessageId>::const_iterator current_message_iterator_; std::vector<MessageId> messages_;
std::vector<MessageId>::const_iterator message_iterator_;
uint32_t last_conversation_start_ = 0; uint32_t last_conversation_start_ = 0;
uint32_t last_conversation_end_ = 0; uint32_t last_conversation_end_ = 0;
@ -78,20 +78,25 @@ class OpenthermHub : public Component {
// Very likely to happen while using Dallas temperature sensors. // Very likely to happen while using Dallas temperature sensors.
bool sync_mode_ = false; bool sync_mode_ = false;
float opentherm_version_ = 0.0f; CallbackManager<void(OpenthermData &)> before_send_callback_;
CallbackManager<void(OpenthermData &)> before_process_response_callback_;
// Create OpenTherm messages based on the message id // Create OpenTherm messages based on the message id
OpenthermData build_request_(MessageId request_id) const; OpenthermData build_request_(MessageId request_id) const;
void handle_protocol_write_error_(); bool handle_error_(OperationMode mode);
void handle_protocol_read_error_(); void handle_protocol_error_();
void handle_timeout_error_(); void handle_timeout_error_();
void handle_timer_error_();
void stop_opentherm_(); void stop_opentherm_();
void start_conversation_(); void start_conversation_();
void read_response_(); void read_response_();
bool check_timings_(uint32_t cur_time); void check_timings_(uint32_t cur_time);
bool should_skip_loop_(uint32_t cur_time) const; bool should_skip_loop_(uint32_t cur_time) const;
void sync_loop_(); void sync_loop_();
void write_initial_messages_(std::vector<MessageId> &target);
void write_repeating_messages_(std::vector<MessageId> &target);
template<typename F> bool spin_wait_(uint32_t timeout, F func) { template<typename F> bool spin_wait_(uint32_t timeout, F func) {
auto start_time = millis(); auto start_time = millis();
while (func()) { while (func()) {
@ -127,13 +132,18 @@ class OpenthermHub : public Component {
OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, )
OPENTHERM_SETTING_LIST(OPENTHERM_SET_SETTING, )
// Add a request to the vector of initial requests // Add a request to the vector of initial requests
void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); } void add_initial_message(MessageId message_id) {
this->configured_messages_[message_id] = INITIAL_UNORDERED_MESSAGE_ORDER;
}
void add_initial_message(MessageId message_id, uint8_t order) { this->configured_messages_[message_id] = order; }
// Add a request to the set of repeating requests. Note that a large number of repeating // Add a request to the set of repeating requests. Note that a large number of repeating
// requests will slow down communication with the boiler. Each request may take up to 1 second, // requests will slow down communication with the boiler. Each request may take up to 1 second,
// so with all sensors enabled, it may take about half a minute before a change in setpoint // so with all sensors enabled, it may take about half a minute before a change in setpoint
// will be processed. // will be processed.
void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); } void add_repeating_message(MessageId message_id) { this->configured_messages_[message_id] = REPEATING_MESSAGE_ORDER; }
// There are seven status variables, which can either be set as a simple variable, // There are seven status variables, which can either be set as a simple variable,
// or using a switch. ch_enable and dhw_enable default to true, the others to false. // or using a switch. ch_enable and dhw_enable default to true, the others to false.
@ -149,7 +159,13 @@ class OpenthermHub : public Component {
void set_summer_mode_active(bool value) { this->summer_mode_active = value; } void set_summer_mode_active(bool value) { this->summer_mode_active = value; }
void set_dhw_block(bool value) { this->dhw_block = value; } void set_dhw_block(bool value) { this->dhw_block = value; }
void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
void set_opentherm_version(float value) { this->opentherm_version_ = value; }
void add_on_before_send_callback(std::function<void(OpenthermData &)> &&callback) {
this->before_send_callback_.add(std::move(callback));
}
void add_on_before_process_response_callback(std::function<void(OpenthermData &)> &&callback) {
this->before_process_response_callback_.add(std::move(callback));
}
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }

View file

@ -52,7 +52,9 @@ bool OpenTherm::initialize() {
OpenTherm::instance = this; OpenTherm::instance = this;
#endif #endif
this->in_pin_->pin_mode(gpio::FLAG_INPUT); this->in_pin_->pin_mode(gpio::FLAG_INPUT);
this->in_pin_->setup();
this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); this->out_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->out_pin_->setup();
this->out_pin_->digital_write(true); this->out_pin_->digital_write(true);
#if defined(ESP32) || defined(USE_ESP_IDF) #if defined(ESP32) || defined(USE_ESP_IDF)
@ -182,7 +184,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
} }
arg->capture_ = 1; // reset counter arg->capture_ = 1; // reset counter
} else if (arg->capture_ > 0xFF) { } else if (arg->capture_ > 0xFF) {
// no change for too long, invalid mancheter encoding // no change for too long, invalid manchester encoding
arg->mode_ = OperationMode::ERROR_PROTOCOL; arg->mode_ = OperationMode::ERROR_PROTOCOL;
arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG;
arg->stop_timer_(); arg->stop_timer_();
@ -312,21 +314,31 @@ bool OpenTherm::init_esp32_timer_() {
} }
void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) {
esp_err_t result; // We will report timer errors outside of interrupt handler
this->timer_error_ = ESP_OK;
this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
if (result != ESP_OK) { if (this->timer_error_ != ESP_OK) {
const auto *error = esp_err_to_name(result); this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR;
ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error); return;
}
this->timer_error_ = timer_start(this->timer_group_, this->timer_idx_);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::TIMER_START_ERROR;
}
}
void OpenTherm::report_and_reset_timer_error() {
if (this->timer_error_ == ESP_OK) {
return; return;
} }
result = timer_start(this->timer_group_, this->timer_idx_); ESP_LOGE(TAG, "Error occured while manipulating timer (%s): %s", this->timer_error_to_str(this->timer_error_type_),
if (result != ESP_OK) { esp_err_to_name(this->timer_error_));
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); this->timer_error_ = ESP_OK;
return; this->timer_error_type_ = NO_TIMER_ERROR;
}
} }
// 5 kHz timer_ // 5 kHz timer_
@ -343,21 +355,18 @@ void IRAM_ATTR OpenTherm::start_write_timer_() {
void IRAM_ATTR OpenTherm::stop_timer_() { void IRAM_ATTR OpenTherm::stop_timer_() {
InterruptLock const lock; InterruptLock const lock;
// We will report timer errors outside of interrupt handler
this->timer_error_ = ESP_OK;
this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
esp_err_t result; this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_);
if (this->timer_error_ != ESP_OK) {
result = timer_pause(this->timer_group_, this->timer_idx_); this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR;
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error);
return; return;
} }
this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); if (this->timer_error_ != ESP_OK) {
if (result != ESP_OK) { this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR;
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error);
return;
} }
} }
@ -386,6 +395,9 @@ void IRAM_ATTR OpenTherm::stop_timer_() {
timer1_detachInterrupt(); timer1_detachInterrupt();
} }
// There is nothing to report on ESP8266
void OpenTherm::report_and_reset_timer_error() {}
#endif // END ESP8266 #endif // END ESP8266
// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd // https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd
@ -412,11 +424,12 @@ const char *OpenTherm::operation_mode_to_str(OperationMode mode) {
TO_STRING_MEMBER(SENT) TO_STRING_MEMBER(SENT)
TO_STRING_MEMBER(ERROR_PROTOCOL) TO_STRING_MEMBER(ERROR_PROTOCOL)
TO_STRING_MEMBER(ERROR_TIMEOUT) TO_STRING_MEMBER(ERROR_TIMEOUT)
TO_STRING_MEMBER(ERROR_TIMER)
default: default:
return "<INVALID>"; return "<INVALID>";
} }
} }
const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { const char *OpenTherm::protocol_error_to_str(ProtocolErrorType error_type) {
switch (error_type) { switch (error_type) {
TO_STRING_MEMBER(NO_ERROR) TO_STRING_MEMBER(NO_ERROR)
TO_STRING_MEMBER(NO_TRANSITION) TO_STRING_MEMBER(NO_TRANSITION)
@ -427,6 +440,17 @@ const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) {
return "<INVALID>"; return "<INVALID>";
} }
} }
const char *OpenTherm::timer_error_to_str(TimerErrorType error_type) {
switch (error_type) {
TO_STRING_MEMBER(NO_TIMER_ERROR)
TO_STRING_MEMBER(SET_ALARM_VALUE_ERROR)
TO_STRING_MEMBER(TIMER_START_ERROR)
TO_STRING_MEMBER(TIMER_PAUSE_ERROR)
TO_STRING_MEMBER(SET_COUNTER_VALUE_ERROR)
default:
return "<INVALID>";
}
}
const char *OpenTherm::message_type_to_str(MessageType message_type) { const char *OpenTherm::message_type_to_str(MessageType message_type) {
switch (message_type) { switch (message_type) {
TO_STRING_MEMBER(READ_DATA) TO_STRING_MEMBER(READ_DATA)

View file

@ -36,11 +36,12 @@ enum OperationMode {
READ = 2, // reading 32-bit data frame READ = 2, // reading 32-bit data frame
RECEIVED = 3, // data frame received with valid start and stop bit RECEIVED = 3, // data frame received with valid start and stop bit
WRITE = 4, // writing data with timer_ WRITE = 4, // writing data to output
SENT = 5, // all data written to output SENT = 5, // all data written to output
ERROR_PROTOCOL = 8, // manchester protocol data transfer error ERROR_PROTOCOL = 8, // protocol error, can happed only during READ
ERROR_TIMEOUT = 9 // read timeout ERROR_TIMEOUT = 9, // timeout while waiting for response from device, only during LISTEN
ERROR_TIMER = 10 // error operating the ESP32 timer
}; };
enum ProtocolErrorType { enum ProtocolErrorType {
@ -51,6 +52,14 @@ enum ProtocolErrorType {
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks
}; };
enum TimerErrorType {
NO_TIMER_ERROR = 0, // No error
SET_ALARM_VALUE_ERROR = 1, // No transition in the middle of the bit
TIMER_START_ERROR = 2, // Stop bit wasn't present when expected
TIMER_PAUSE_ERROR = 3, // Parity check didn't pass
SET_COUNTER_VALUE_ERROR = 4, // No level change for too much timer ticks
};
enum MessageType { enum MessageType {
READ_DATA = 0, READ_DATA = 0,
READ_ACK = 4, READ_ACK = 4,
@ -299,7 +308,9 @@ class OpenTherm {
* *
* @return true if last listen() or send() operation ends up with an error. * @return true if last listen() or send() operation ends up with an error.
*/ */
bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; } bool is_error() {
return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL || mode_ == ERROR_TIMER;
}
/** /**
* Indicates whether last listen() or send() operation ends up with a *timeout* error * Indicates whether last listen() or send() operation ends up with a *timeout* error
@ -313,14 +324,22 @@ class OpenTherm {
*/ */
bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; } bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; }
/**
* Indicates whether start_esp32_timer_() or stop_timer_() had an error. Only relevant when used on ESP32.
* @return true if there was an error.
*/
bool is_timer_error() { return mode_ == OperationMode::ERROR_TIMER; }
bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; } bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
OperationMode get_mode() { return mode_; } OperationMode get_mode() { return mode_; }
void debug_data(OpenthermData &data); void debug_data(OpenthermData &data);
void debug_error(OpenThermError &error) const; void debug_error(OpenThermError &error) const;
void report_and_reset_timer_error();
const char *protocol_error_to_to_str(ProtocolErrorType error_type); const char *protocol_error_to_str(ProtocolErrorType error_type);
const char *timer_error_to_str(TimerErrorType error_type);
const char *message_type_to_str(MessageType message_type); const char *message_type_to_str(MessageType message_type);
const char *operation_mode_to_str(OperationMode mode); const char *operation_mode_to_str(OperationMode mode);
const char *message_id_to_str(MessageId id); const char *message_id_to_str(MessageId id);
@ -349,10 +368,12 @@ class OpenTherm {
uint32_t data_; uint32_t data_;
uint8_t bit_pos_; uint8_t bit_pos_;
int32_t timeout_counter_; // <0 no timeout int32_t timeout_counter_; // <0 no timeout
int32_t device_timeout_; int32_t device_timeout_;
#if defined(ESP32) || defined(USE_ESP_IDF) #if defined(ESP32) || defined(USE_ESP_IDF)
esp_err_t timer_error_ = ESP_OK;
TimerErrorType timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
bool init_esp32_timer_(); bool init_esp32_timer_();
void start_esp32_timer_(uint64_t alarm_value); void start_esp32_timer_(uint64_t alarm_value);
#endif #endif

View file

@ -28,6 +28,9 @@ namespace opentherm {
#ifndef OPENTHERM_INPUT_SENSOR_LIST #ifndef OPENTHERM_INPUT_SENSOR_LIST
#define OPENTHERM_INPUT_SENSOR_LIST(F, sep) #define OPENTHERM_INPUT_SENSOR_LIST(F, sep)
#endif #endif
#ifndef OPENTHERM_SETTING_LIST
#define OPENTHERM_SETTING_LIST(F, sep)
#endif
// Use macros to create fields for every entity specified in the ESPHome configuration // Use macros to create fields for every entity specified in the ESPHome configuration
#define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity;
@ -36,6 +39,7 @@ namespace opentherm {
#define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity;
#define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity;
#define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity;
#define OPENTHERM_DECLARE_SETTING(type, entity, def) type entity = def;
// Setter macros // Setter macros
#define OPENTHERM_SET_SENSOR(entity) \ #define OPENTHERM_SET_SENSOR(entity) \
@ -56,6 +60,9 @@ namespace opentherm {
#define OPENTHERM_SET_INPUT_SENSOR(entity) \ #define OPENTHERM_SET_INPUT_SENSOR(entity) \
void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; }
#define OPENTHERM_SET_SETTING(type, entity, def) \
void set_##entity(type value) { this->entity = value; }
// ===== hub.cpp macros ===== // ===== hub.cpp macros =====
// *_MESSAGE_HANDLERS are generated in defines.h and look like this: // *_MESSAGE_HANDLERS are generated in defines.h and look like this:
@ -85,6 +92,9 @@ namespace opentherm {
#ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS
#define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
#endif #endif
#ifndef OPENTHERM_SETTING_MESSAGE_HANDLERS
#define OPENTHERM_SETTING_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
#endif
// Write data request builders // Write data request builders
#define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \
@ -92,6 +102,7 @@ namespace opentherm {
data.type = MessageType::WRITE_DATA; \ data.type = MessageType::WRITE_DATA; \
data.id = request_id; data.id = request_id;
#define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data); #define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data);
#define OPENTHERM_MESSAGE_WRITE_SETTING(key, msg_data) message_data::write_##msg_data(this->key, data);
#define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \ #define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \
return data; \ return data; \
} }

View file

@ -2,8 +2,9 @@
# inputs of the OpenTherm component. # inputs of the OpenTherm component.
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, TypeVar from typing import Optional, TypeVar, Any
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_EMPTY, UNIT_EMPTY,
@ -64,6 +65,7 @@ class SensorSchema(EntitySchema):
icon: Optional[str] = None icon: Optional[str] = None
device_class: Optional[str] = None device_class: Optional[str] = None
disabled_by_default: bool = False disabled_by_default: bool = False
order: Optional[int] = None
SENSORS: dict[str, SensorSchema] = { SENSORS: dict[str, SensorSchema] = {
@ -399,6 +401,7 @@ SENSORS: dict[str, SensorSchema] = {
message="OT_VERSION_DEVICE", message="OT_VERSION_DEVICE",
keep_updated=False, keep_updated=False,
message_data="f88", message_data="f88",
order=2,
), ),
"device_type": SensorSchema( "device_type": SensorSchema(
description="Device product type", description="Device product type",
@ -409,6 +412,7 @@ SENSORS: dict[str, SensorSchema] = {
message="VERSION_DEVICE", message="VERSION_DEVICE",
keep_updated=False, keep_updated=False,
message_data="u8_hb", message_data="u8_hb",
order=0,
), ),
"device_version": SensorSchema( "device_version": SensorSchema(
description="Device product version", description="Device product version",
@ -419,6 +423,7 @@ SENSORS: dict[str, SensorSchema] = {
message="VERSION_DEVICE", message="VERSION_DEVICE",
keep_updated=False, keep_updated=False,
message_data="u8_lb", message_data="u8_lb",
order=0,
), ),
"device_id": SensorSchema( "device_id": SensorSchema(
description="Device ID code", description="Device ID code",
@ -429,6 +434,7 @@ SENSORS: dict[str, SensorSchema] = {
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="u8_lb", message_data="u8_lb",
order=4,
), ),
"otc_hc_ratio_ub": SensorSchema( "otc_hc_ratio_ub": SensorSchema(
description="OTC heat curve ratio upper bound", description="OTC heat curve ratio upper bound",
@ -457,6 +463,7 @@ SENSORS: dict[str, SensorSchema] = {
class BinarySensorSchema(EntitySchema): class BinarySensorSchema(EntitySchema):
icon: Optional[str] = None icon: Optional[str] = None
device_class: Optional[str] = None device_class: Optional[str] = None
order: Optional[int] = None
BINARY_SENSORS: dict[str, BinarySensorSchema] = { BINARY_SENSORS: dict[str, BinarySensorSchema] = {
@ -525,48 +532,56 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = {
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_0", message_data="flag8_hb_0",
order=4,
), ),
"control_type_on_off": BinarySensorSchema( "control_type_on_off": BinarySensorSchema(
description="Configuration: Control type is on/off", description="Configuration: Control type is on/off",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_1", message_data="flag8_hb_1",
order=4,
), ),
"cooling_supported": BinarySensorSchema( "cooling_supported": BinarySensorSchema(
description="Configuration: Cooling supported", description="Configuration: Cooling supported",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_2", message_data="flag8_hb_2",
order=4,
), ),
"dhw_storage_tank": BinarySensorSchema( "dhw_storage_tank": BinarySensorSchema(
description="Configuration: DHW storage tank", description="Configuration: DHW storage tank",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_3", message_data="flag8_hb_3",
order=4,
), ),
"controller_pump_control_allowed": BinarySensorSchema( "controller_pump_control_allowed": BinarySensorSchema(
description="Configuration: Controller pump control allowed", description="Configuration: Controller pump control allowed",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_4", message_data="flag8_hb_4",
order=4,
), ),
"ch2_present": BinarySensorSchema( "ch2_present": BinarySensorSchema(
description="Configuration: CH2 present", description="Configuration: CH2 present",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_5", message_data="flag8_hb_5",
order=4,
), ),
"water_filling": BinarySensorSchema( "water_filling": BinarySensorSchema(
description="Configuration: Remote water filling", description="Configuration: Remote water filling",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_6", message_data="flag8_hb_6",
order=4,
), ),
"heat_mode": BinarySensorSchema( "heat_mode": BinarySensorSchema(
description="Configuration: Heating or cooling", description="Configuration: Heating or cooling",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_7", message_data="flag8_hb_7",
order=4,
), ),
"dhw_setpoint_transfer_enabled": BinarySensorSchema( "dhw_setpoint_transfer_enabled": BinarySensorSchema(
description="Remote boiler parameters: DHW setpoint transfer enabled", description="Remote boiler parameters: DHW setpoint transfer enabled",
@ -812,3 +827,65 @@ INPUTS: dict[str, InputSchema] = {
auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"), auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"),
), ),
} }
@dataclass
class SettingSchema(EntitySchema):
backing_type: str
validation_schema: cv.Schema
default_value: Any
order: Optional[int] = None
SETTINGS: dict[str, SettingSchema] = {
"controller_product_type": SettingSchema(
description="Controller product type",
message="VERSION_CONTROLLER",
keep_updated=False,
message_data="u8_hb",
backing_type="uint8_t",
validation_schema=cv.int_range(min=0, max=255),
default_value=0,
order=1,
),
"controller_product_version": SettingSchema(
description="Controller product version",
message="VERSION_CONTROLLER",
keep_updated=False,
message_data="u8_lb",
backing_type="uint8_t",
validation_schema=cv.int_range(min=0, max=255),
default_value=0,
order=1,
),
"opentherm_version_controller": SettingSchema(
description="Version of OpenTherm implemented by controller",
message="OT_VERSION_CONTROLLER",
keep_updated=False,
message_data="f88",
backing_type="float",
validation_schema=cv.positive_float,
default_value=0,
order=3,
),
"controller_configuration": SettingSchema(
description="Controller configuration",
message="CONTROLLER_CONFIG",
keep_updated=False,
message_data="u8_hb",
backing_type="uint8_t",
validation_schema=cv.int_range(min=0, max=255),
default_value=0,
order=5,
),
"controller_id": SettingSchema(
description="Controller ID code",
message="CONTROLLER_CONFIG",
keep_updated=False,
message_data="u8_lb",
backing_type="uint8_t",
validation_schema=cv.int_range(min=0, max=255),
default_value=0,
order=5,
),
}

View file

@ -9,12 +9,17 @@ from .schema import TSchema
def create_entities_schema( def create_entities_schema(
entities: dict[str, schema.EntitySchema], entities: dict[str, TSchema],
get_entity_validation_schema: Callable[[TSchema], cv.Schema], get_entity_validation_schema: Callable[[TSchema], cv.Schema],
) -> Schema: ) -> Schema:
entity_schema = {} entity_schema = {}
for key, entity in entities.items(): for key, entity in entities.items():
entity_schema[cv.Optional(key)] = get_entity_validation_schema(entity) schema_key = (
cv.Optional(key, entity.default_value)
if hasattr(entity, "default_value")
else cv.Optional(key)
)
entity_schema[schema_key] = get_entity_validation_schema(entity)
return cv.Schema(entity_schema) return cv.Schema(entity_schema)

View file

@ -16,6 +16,19 @@ opentherm:
summer_mode_active: true summer_mode_active: true
dhw_block: true dhw_block: true
sync_mode: true sync_mode: true
controller_product_type: 63
controller_product_version: 1
opentherm_version_controller: 2.2
controller_id: 1
controller_configuration: 1
before_send:
then:
- lambda: |-
ESP_LOGW("OT", ">> Sending message %d", x.id);
before_process_response:
then:
- lambda: |-
ESP_LOGW("OT", "<< Processing response %d", x.id);
output: output:
- platform: opentherm - platform: opentherm