mirror of
https://github.com/esphome/esphome.git
synced 2026-05-21 02:01:57 +08:00
Add OpenTherm component (part 1: communication layer and hub) (#6645)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
@@ -289,6 +289,7 @@ esphome/components/noblex/* @AGalfra
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/one_wire/* @ssieb
|
||||
esphome/components/online_image/* @guillempages
|
||||
esphome/components/opentherm/* @olegtarasov
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
||||
|
||||
CODEOWNERS = ["@olegtarasov"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_IN_PIN = "in_pin"
|
||||
CONF_OUT_PIN = "out_pin"
|
||||
CONF_CH_ENABLE = "ch_enable"
|
||||
CONF_DHW_ENABLE = "dhw_enable"
|
||||
CONF_COOLING_ENABLE = "cooling_enable"
|
||||
CONF_OTC_ACTIVE = "otc_active"
|
||||
CONF_CH2_ACTIVE = "ch2_active"
|
||||
CONF_SYNC_MODE = "sync_mode"
|
||||
|
||||
opentherm_ns = cg.esphome_ns.namespace("opentherm")
|
||||
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(OpenthermHub),
|
||||
cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Optional(CONF_CH_ENABLE, True): cv.boolean,
|
||||
cv.Optional(CONF_DHW_ENABLE, True): cv.boolean,
|
||||
cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean,
|
||||
cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean,
|
||||
cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean,
|
||||
cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: dict[str, Any]) -> None:
|
||||
# Create the hub, passing the two callbacks defined below
|
||||
# Since the hub is used in the callbacks, we need to define it first
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Set pins
|
||||
in_pin = await cg.gpio_pin_expression(config[CONF_IN_PIN])
|
||||
cg.add(var.set_in_pin(in_pin))
|
||||
|
||||
out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN])
|
||||
cg.add(var.set_out_pin(out_pin))
|
||||
|
||||
non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
|
||||
for key, value in config.items():
|
||||
if key not in non_sensors:
|
||||
cg.add(getattr(var, f"set_{key}")(value))
|
||||
@@ -0,0 +1,277 @@
|
||||
#include "hub.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace opentherm {
|
||||
|
||||
static const char *const TAG = "opentherm";
|
||||
|
||||
OpenthermData OpenthermHub::build_request_(MessageId request_id) {
|
||||
OpenthermData data;
|
||||
data.type = 0;
|
||||
data.id = 0;
|
||||
data.valueHB = 0;
|
||||
data.valueLB = 0;
|
||||
|
||||
// First, handle the status request. This requires special logic, because we
|
||||
// wouldn't want to inadvertently disable domestic hot water, for example.
|
||||
// It is also included in the macro-generated code below, but that will
|
||||
// never be executed, because we short-circuit it here.
|
||||
if (request_id == MessageId::STATUS) {
|
||||
bool const ch_enabled = this->ch_enable;
|
||||
bool dhw_enabled = this->dhw_enable;
|
||||
bool cooling_enabled = this->cooling_enable;
|
||||
bool otc_enabled = this->otc_active;
|
||||
bool ch2_enabled = this->ch2_active;
|
||||
|
||||
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);
|
||||
|
||||
// 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"
|
||||
|
||||
// TODO: This is a placeholder for an auto-generated switch statement which builds request structure based on
|
||||
// which sensors are enabled in config.
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
return data;
|
||||
}
|
||||
return OpenthermData();
|
||||
}
|
||||
|
||||
OpenthermHub::OpenthermHub() : Component() {}
|
||||
|
||||
void OpenthermHub::process_response(OpenthermData &data) {
|
||||
ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
|
||||
this->opentherm_->message_id_to_str((MessageId) data.id));
|
||||
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
|
||||
}
|
||||
|
||||
void OpenthermHub::setup() {
|
||||
ESP_LOGD(TAG, "Setting up OpenTherm component");
|
||||
this->opentherm_ = make_unique<OpenTherm>(this->in_pin_, this->out_pin_);
|
||||
if (!this->opentherm_->initialize()) {
|
||||
ESP_LOGE(TAG, "Failed to initialize OpenTherm protocol. See previous log messages for details.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that there is at least one request, as we are required to
|
||||
// communicate at least once every second. Sending the status request is
|
||||
// good practice anyway.
|
||||
this->add_repeating_message(MessageId::STATUS);
|
||||
|
||||
this->current_message_iterator_ = this->initial_messages_.begin();
|
||||
}
|
||||
|
||||
void OpenthermHub::on_shutdown() { this->opentherm_->stop(); }
|
||||
|
||||
void OpenthermHub::loop() {
|
||||
if (this->sync_mode_) {
|
||||
this->sync_loop_();
|
||||
return;
|
||||
}
|
||||
|
||||
auto cur_time = millis();
|
||||
auto const cur_mode = this->opentherm_->get_mode();
|
||||
switch (cur_mode) {
|
||||
case OperationMode::WRITE:
|
||||
case OperationMode::READ:
|
||||
case OperationMode::LISTEN:
|
||||
if (!this->check_timings_(cur_time)) {
|
||||
break;
|
||||
}
|
||||
this->last_mode_ = cur_mode;
|
||||
break;
|
||||
case OperationMode::ERROR_PROTOCOL:
|
||||
if (this->last_mode_ == OperationMode::WRITE) {
|
||||
this->handle_protocol_write_error_();
|
||||
} else if (this->last_mode_ == OperationMode::READ) {
|
||||
this->handle_protocol_read_error_();
|
||||
}
|
||||
|
||||
this->stop_opentherm_();
|
||||
break;
|
||||
case OperationMode::ERROR_TIMEOUT:
|
||||
this->handle_timeout_error_();
|
||||
this->stop_opentherm_();
|
||||
break;
|
||||
case OperationMode::IDLE:
|
||||
if (this->should_skip_loop_(cur_time)) {
|
||||
break;
|
||||
}
|
||||
this->start_conversation_();
|
||||
break;
|
||||
case OperationMode::SENT:
|
||||
// Message sent, now listen for the response.
|
||||
this->opentherm_->listen();
|
||||
break;
|
||||
case OperationMode::RECEIVED:
|
||||
this->read_response_();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenthermHub::sync_loop_() {
|
||||
if (!this->opentherm_->is_idle()) {
|
||||
ESP_LOGE(TAG, "OpenTherm is not idle at the start of the loop");
|
||||
return;
|
||||
}
|
||||
|
||||
auto cur_time = millis();
|
||||
|
||||
this->check_timings_(cur_time);
|
||||
|
||||
if (this->should_skip_loop_(cur_time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->start_conversation_();
|
||||
|
||||
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
|
||||
ESP_LOGE(TAG, "Hub timeout triggered during send");
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->opentherm_->is_error()) {
|
||||
this->handle_protocol_write_error_();
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
} else if (!this->opentherm_->is_sent()) {
|
||||
ESP_LOGW(TAG, "Unexpected state after sending request: %s",
|
||||
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for the response
|
||||
this->opentherm_->listen();
|
||||
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
|
||||
ESP_LOGE(TAG, "Hub timeout triggered during receive");
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->opentherm_->is_timeout()) {
|
||||
this->handle_timeout_error_();
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
} else if (this->opentherm_->is_protocol_error()) {
|
||||
this->handle_protocol_read_error_();
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
} else if (!this->opentherm_->has_message()) {
|
||||
ESP_LOGW(TAG, "Unexpected state after receiving response: %s",
|
||||
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
}
|
||||
|
||||
this->read_response_();
|
||||
}
|
||||
|
||||
bool OpenthermHub::check_timings_(uint32_t cur_time) {
|
||||
if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) {
|
||||
ESP_LOGW(TAG,
|
||||
"%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other "
|
||||
"components that might slow the loop down.",
|
||||
(int) (cur_time - this->last_conversation_start_));
|
||||
this->stop_opentherm_();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
|
||||
if (this->last_conversation_end_ > 0 && (cur_time - this->last_conversation_end_) < 100) {
|
||||
ESP_LOGV(TAG, "Less than 100 ms elapsed since last convo, skipping this iteration");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OpenthermHub::start_conversation_() {
|
||||
if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) {
|
||||
this->sending_initial_ = false;
|
||||
this->current_message_iterator_ = this->repeating_messages_.begin();
|
||||
} else if (this->current_message_iterator_ == this->repeating_messages_.end()) {
|
||||
this->current_message_iterator_ = this->repeating_messages_.begin();
|
||||
}
|
||||
|
||||
auto request = this->build_request_(*this->current_message_iterator_);
|
||||
|
||||
ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
|
||||
this->opentherm_->message_id_to_str((MessageId) request.id));
|
||||
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str());
|
||||
// Send the request
|
||||
this->last_conversation_start_ = millis();
|
||||
this->opentherm_->send(request);
|
||||
}
|
||||
|
||||
void OpenthermHub::read_response_() {
|
||||
OpenthermData response;
|
||||
if (!this->opentherm_->get_message(response)) {
|
||||
ESP_LOGW(TAG, "Couldn't get the response, but flags indicated success. This is a bug.");
|
||||
this->stop_opentherm_();
|
||||
return;
|
||||
}
|
||||
|
||||
this->stop_opentherm_();
|
||||
|
||||
this->process_response(response);
|
||||
|
||||
this->current_message_iterator_++;
|
||||
}
|
||||
|
||||
void OpenthermHub::stop_opentherm_() {
|
||||
this->opentherm_->stop();
|
||||
this->last_conversation_end_ = millis();
|
||||
}
|
||||
|
||||
void OpenthermHub::handle_protocol_write_error_() {
|
||||
ESP_LOGW(TAG, "Error while sending request: %s",
|
||||
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||
ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str());
|
||||
}
|
||||
|
||||
void OpenthermHub::handle_protocol_read_error_() {
|
||||
OpenThermError error;
|
||||
this->opentherm_->get_protocol_error(error);
|
||||
ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str());
|
||||
}
|
||||
|
||||
void OpenthermHub::handle_timeout_error_() {
|
||||
ESP_LOGW(TAG, "Receive response timed out at a protocol level");
|
||||
this->stop_opentherm_();
|
||||
}
|
||||
|
||||
#define ID(x) x
|
||||
#define SHOW2(x) #x
|
||||
#define SHOW(x) SHOW2(x)
|
||||
|
||||
void OpenthermHub::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "OpenTherm:");
|
||||
LOG_PIN(" In: ", this->in_pin_);
|
||||
LOG_PIN(" Out: ", this->out_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_);
|
||||
ESP_LOGCONFIG(TAG, " Initial requests:");
|
||||
for (auto type : this->initial_messages_) {
|
||||
ESP_LOGCONFIG(TAG, " - %d", type);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Repeating requests:");
|
||||
for (auto type : this->repeating_messages_) {
|
||||
ESP_LOGCONFIG(TAG, " - %d", type);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace opentherm
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "opentherm.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <functional>
|
||||
|
||||
namespace esphome {
|
||||
namespace opentherm {
|
||||
|
||||
// OpenTherm component for ESPHome
|
||||
class OpenthermHub : public Component {
|
||||
protected:
|
||||
// Communication pins for the OpenTherm interface
|
||||
InternalGPIOPin *in_pin_, *out_pin_;
|
||||
// The OpenTherm interface
|
||||
std::unique_ptr<OpenTherm> opentherm_;
|
||||
|
||||
// The set of initial messages to send on starting communication with the boiler
|
||||
std::unordered_set<MessageId> initial_messages_;
|
||||
// and the repeating messages which are sent repeatedly to update various sensors
|
||||
// and boiler parameters (like the setpoint).
|
||||
std::unordered_set<MessageId> repeating_messages_;
|
||||
// Indicates if we are still working on the initial requests or not
|
||||
bool sending_initial_ = true;
|
||||
// Index for the current request in one of the _requests sets.
|
||||
std::unordered_set<MessageId>::const_iterator current_message_iterator_;
|
||||
|
||||
uint32_t last_conversation_start_ = 0;
|
||||
uint32_t last_conversation_end_ = 0;
|
||||
OperationMode last_mode_ = IDLE;
|
||||
OpenthermData last_request_;
|
||||
|
||||
// Synchronous communication mode prevents other components from disabling interrupts while
|
||||
// we are talking to the boiler. Enable if you experience random intermittent invalid response errors.
|
||||
// Very likely to happen while using Dallas temperature sensors.
|
||||
bool sync_mode_ = false;
|
||||
|
||||
// Create OpenTherm messages based on the message id
|
||||
OpenthermData build_request_(MessageId request_id);
|
||||
void handle_protocol_write_error_();
|
||||
void handle_protocol_read_error_();
|
||||
void handle_timeout_error_();
|
||||
void stop_opentherm_();
|
||||
void start_conversation_();
|
||||
void read_response_();
|
||||
bool check_timings_(uint32_t cur_time);
|
||||
bool should_skip_loop_(uint32_t cur_time) const;
|
||||
void sync_loop_();
|
||||
|
||||
template<typename F> bool spin_wait_(uint32_t timeout, F func) {
|
||||
auto start_time = millis();
|
||||
while (func()) {
|
||||
yield();
|
||||
auto cur_time = millis();
|
||||
if (cur_time - start_time >= timeout) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
// Constructor with references to the global interrupt handlers
|
||||
OpenthermHub();
|
||||
|
||||
// Handle responses from the OpenTherm interface
|
||||
void process_response(OpenthermData &data);
|
||||
|
||||
// Setters for the input and output OpenTherm interface pins
|
||||
void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; }
|
||||
void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; }
|
||||
|
||||
// Add a request to the set of initial requests
|
||||
void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); }
|
||||
// Add a request to the set of repeating requests. Note that a large number of repeating
|
||||
// requests will slow down communication with the boiler. Each request may take up to 1 second,
|
||||
// so with all sensors enabled, it may take about half a minute before a change in setpoint
|
||||
// will be processed.
|
||||
void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); }
|
||||
|
||||
// There are five 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.
|
||||
bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false;
|
||||
|
||||
// Setters for the status variables
|
||||
void set_ch_enable(bool value) { this->ch_enable = value; }
|
||||
void set_dhw_enable(bool value) { this->dhw_enable = value; }
|
||||
void set_cooling_enable(bool value) { this->cooling_enable = value; }
|
||||
void set_otc_active(bool value) { this->otc_active = value; }
|
||||
void set_ch2_active(bool value) { this->ch2_active = value; }
|
||||
void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void setup() override;
|
||||
void on_shutdown() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace opentherm
|
||||
} // namespace esphome
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
|
||||
* heavily modified to comply with ESPHome coding standards and provide better logging.
|
||||
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||
#include "driver/timer.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace opentherm {
|
||||
|
||||
// TODO: Account for immutable semantics change in hub.cpp when doing later installments of OpenTherm PR
|
||||
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
|
||||
|
||||
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
|
||||
|
||||
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
|
||||
|
||||
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
|
||||
return bit_value ? setBit(value, bit) : clearBit(value, bit);
|
||||
}
|
||||
|
||||
enum OperationMode {
|
||||
IDLE = 0, // no operation
|
||||
|
||||
LISTEN = 1, // waiting for transmission to start
|
||||
READ = 2, // reading 32-bit data frame
|
||||
RECEIVED = 3, // data frame received with valid start and stop bit
|
||||
|
||||
WRITE = 4, // writing data with timer_
|
||||
SENT = 5, // all data written to output
|
||||
|
||||
ERROR_PROTOCOL = 8, // manchester protocol data transfer error
|
||||
ERROR_TIMEOUT = 9 // read timeout
|
||||
};
|
||||
|
||||
enum ProtocolErrorType {
|
||||
NO_ERROR = 0, // No error
|
||||
NO_TRANSITION = 1, // No transition in the middle of the bit
|
||||
INVALID_STOP_BIT = 2, // Stop bit wasn't present when expected
|
||||
PARITY_ERROR = 3, // Parity check didn't pass
|
||||
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks
|
||||
};
|
||||
|
||||
enum MessageType {
|
||||
READ_DATA = 0,
|
||||
READ_ACK = 4,
|
||||
WRITE_DATA = 1,
|
||||
WRITE_ACK = 5,
|
||||
INVALID_DATA = 2,
|
||||
DATA_INVALID = 6,
|
||||
UNKNOWN_DATAID = 7
|
||||
};
|
||||
|
||||
enum MessageId {
|
||||
STATUS = 0,
|
||||
CH_SETPOINT = 1,
|
||||
CONTROLLER_CONFIG = 2,
|
||||
DEVICE_CONFIG = 3,
|
||||
COMMAND_CODE = 4,
|
||||
FAULT_FLAGS = 5,
|
||||
REMOTE = 6,
|
||||
COOLING_CONTROL = 7,
|
||||
CH2_SETPOINT = 8,
|
||||
CH_SETPOINT_OVERRIDE = 9,
|
||||
TSP_COUNT = 10,
|
||||
TSP_COMMAND = 11,
|
||||
FHB_SIZE = 12,
|
||||
FHB_COMMAND = 13,
|
||||
MAX_MODULATION_LEVEL = 14,
|
||||
MAX_BOILER_CAPACITY = 15, // u8_hb - u8_lb gives min modulation level
|
||||
ROOM_SETPOINT = 16,
|
||||
MODULATION_LEVEL = 17,
|
||||
CH_WATER_PRESSURE = 18,
|
||||
DHW_FLOW_RATE = 19,
|
||||
DAY_TIME = 20,
|
||||
DATE = 21,
|
||||
YEAR = 22,
|
||||
ROOM_SETPOINT_CH2 = 23,
|
||||
ROOM_TEMP = 24,
|
||||
FEED_TEMP = 25,
|
||||
DHW_TEMP = 26,
|
||||
OUTSIDE_TEMP = 27,
|
||||
RETURN_WATER_TEMP = 28,
|
||||
SOLAR_STORE_TEMP = 29,
|
||||
SOLAR_COLLECT_TEMP = 30,
|
||||
FEED_TEMP_CH2 = 31,
|
||||
DHW2_TEMP = 32,
|
||||
EXHAUST_TEMP = 33,
|
||||
FAN_SPEED = 35,
|
||||
FLAME_CURRENT = 36,
|
||||
DHW_BOUNDS = 48,
|
||||
CH_BOUNDS = 49,
|
||||
OTC_CURVE_BOUNDS = 50,
|
||||
DHW_SETPOINT = 56,
|
||||
MAX_CH_SETPOINT = 57,
|
||||
OTC_CURVE_RATIO = 58,
|
||||
|
||||
// HVAC Specific Message IDs
|
||||
HVAC_STATUS = 70,
|
||||
REL_VENT_SETPOINT = 71,
|
||||
DEVICE_VENT = 74,
|
||||
REL_VENTILATION = 77,
|
||||
REL_HUMID_EXHAUST = 78,
|
||||
SUPPLY_INLET_TEMP = 80,
|
||||
SUPPLY_OUTLET_TEMP = 81,
|
||||
EXHAUST_INLET_TEMP = 82,
|
||||
EXHAUST_OUTLET_TEMP = 83,
|
||||
NOM_REL_VENTILATION = 87,
|
||||
|
||||
OVERRIDE_FUNC = 100,
|
||||
OEM_DIAGNOSTIC = 115,
|
||||
BURNER_STARTS = 116,
|
||||
CH_PUMP_STARTS = 117,
|
||||
DHW_PUMP_STARTS = 118,
|
||||
DHW_BURNER_STARTS = 119,
|
||||
BURNER_HOURS = 120,
|
||||
CH_PUMP_HOURS = 121,
|
||||
DHW_PUMP_HOURS = 122,
|
||||
DHW_BURNER_HOURS = 123,
|
||||
OT_VERSION_CONTROLLER = 124,
|
||||
OT_VERSION_DEVICE = 125,
|
||||
VERSION_CONTROLLER = 126,
|
||||
VERSION_DEVICE = 127
|
||||
};
|
||||
|
||||
enum BitPositions { STOP_BIT = 33 };
|
||||
|
||||
/**
|
||||
* Structure to hold Opentherm data packet content.
|
||||
* Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message.
|
||||
*/
|
||||
struct OpenthermData {
|
||||
uint8_t type;
|
||||
uint8_t id;
|
||||
uint8_t valueHB;
|
||||
uint8_t valueLB;
|
||||
|
||||
OpenthermData() : type(0), id(0), valueHB(0), valueLB(0) {}
|
||||
|
||||
/**
|
||||
* @return float representation of data packet value
|
||||
*/
|
||||
float f88();
|
||||
|
||||
/**
|
||||
* @param float number to set as value of this data packet
|
||||
*/
|
||||
void f88(float value);
|
||||
|
||||
/**
|
||||
* @return unsigned 16b integer representation of data packet value
|
||||
*/
|
||||
uint16_t u16();
|
||||
|
||||
/**
|
||||
* @param unsigned 16b integer number to set as value of this data packet
|
||||
*/
|
||||
void u16(uint16_t value);
|
||||
|
||||
/**
|
||||
* @return signed 16b integer representation of data packet value
|
||||
*/
|
||||
int16_t s16();
|
||||
|
||||
/**
|
||||
* @param signed 16b integer number to set as value of this data packet
|
||||
*/
|
||||
void s16(int16_t value);
|
||||
};
|
||||
|
||||
struct OpenThermError {
|
||||
ProtocolErrorType error_type;
|
||||
uint32_t capture;
|
||||
uint8_t clock;
|
||||
uint32_t data;
|
||||
uint8_t bit_pos;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opentherm static class that supports either listening or sending Opentherm data packets in the same time
|
||||
*/
|
||||
class OpenTherm {
|
||||
public:
|
||||
OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout = 800);
|
||||
|
||||
/**
|
||||
* Setup pins.
|
||||
*/
|
||||
bool initialize();
|
||||
|
||||
/**
|
||||
* Start listening for Opentherm data packet comming from line connected to given pin.
|
||||
* If data packet is received then has_message() function returns true and data packet can be retrieved by calling
|
||||
* get_message() function. If timeout > 0 then this function waits for incomming data package for timeout millis and
|
||||
* if no data packet is recevived, error state is indicated by is_error() function. If either data packet is received
|
||||
* or timeout is reached listening is stopped.
|
||||
*/
|
||||
void listen();
|
||||
|
||||
/**
|
||||
* Use this function to check whether listen() function already captured a valid data packet.
|
||||
*
|
||||
* @return true if data packet has been captured from line by listen() function.
|
||||
*/
|
||||
bool has_message() { return mode_ == OperationMode::RECEIVED; }
|
||||
|
||||
/**
|
||||
* Use this to retrive data packed captured by listen() function. Data packet is ready when has_message() function
|
||||
* returns true. This function can be called multiple times until stop() is called.
|
||||
*
|
||||
* @param data reference to data structure to which fill the data packet data.
|
||||
* @return true if packet was ready and was filled into data structure passed, false otherwise.
|
||||
*/
|
||||
bool get_message(OpenthermData &data);
|
||||
|
||||
/**
|
||||
* Immediately send out Opentherm data packet to line connected on given pin.
|
||||
* Completed data transfer is indicated by is_sent() function.
|
||||
* Error state is indicated by is_error() function.
|
||||
*
|
||||
* @param data Opentherm data packet.
|
||||
*/
|
||||
void send(OpenthermData &data);
|
||||
|
||||
/**
|
||||
* Stops listening for data packet or sending out data packet and resets internal state of this class.
|
||||
* Stops all timers and unattaches all interrupts.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Get protocol error details in case a protocol error occured.
|
||||
* @param error reference to data structure to which fill the error details
|
||||
* @return true if protocol error occured during last conversation, false otherwise.
|
||||
*/
|
||||
bool get_protocol_error(OpenThermError &error);
|
||||
|
||||
/**
|
||||
* Use this function to check whether send() function already finished sending data packed to line.
|
||||
*
|
||||
* @return true if data packet has been sent, false otherwise.
|
||||
*/
|
||||
bool is_sent() { return mode_ == OperationMode::SENT; }
|
||||
|
||||
/**
|
||||
* Indicates whether listinig or sending is not in progress.
|
||||
* That also means that no timers are running and no interrupts are attached.
|
||||
*
|
||||
* @return true if listening nor sending is in progress.
|
||||
*/
|
||||
bool is_idle() { return mode_ == OperationMode::IDLE; }
|
||||
|
||||
/**
|
||||
* Indicates whether last listen() or send() operation ends up with an error. Includes both timeout and
|
||||
* protocol errors.
|
||||
*
|
||||
* @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; }
|
||||
|
||||
/**
|
||||
* Indicates whether last listen() or send() operation ends up with a *timeout* error
|
||||
* @return true if last listen() or send() operation ends up with a *timeout* error.
|
||||
*/
|
||||
bool is_timeout() { return mode_ == OperationMode::ERROR_TIMEOUT; }
|
||||
|
||||
/**
|
||||
* Indicates whether last listen() or send() operation ends up with a *protocol* error
|
||||
* @return true if last listen() or send() operation ends up with a *protocol* error.
|
||||
*/
|
||||
bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; }
|
||||
|
||||
bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
|
||||
|
||||
OperationMode get_mode() { return mode_; }
|
||||
|
||||
std::string debug_data(OpenthermData &data);
|
||||
std::string debug_error(OpenThermError &error);
|
||||
|
||||
const char *protocol_error_to_to_str(ProtocolErrorType error_type);
|
||||
const char *message_type_to_str(MessageType message_type);
|
||||
const char *operation_mode_to_str(OperationMode mode);
|
||||
const char *message_id_to_str(MessageId id);
|
||||
|
||||
static bool timer_isr(OpenTherm *arg);
|
||||
|
||||
#ifdef ESP8266
|
||||
static void esp8266_timer_isr();
|
||||
#endif
|
||||
|
||||
private:
|
||||
InternalGPIOPin *in_pin_;
|
||||
InternalGPIOPin *out_pin_;
|
||||
ISRInternalGPIOPin isr_in_pin_;
|
||||
ISRInternalGPIOPin isr_out_pin_;
|
||||
|
||||
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||
timer_group_t timer_group_;
|
||||
timer_idx_t timer_idx_;
|
||||
#endif
|
||||
|
||||
OperationMode mode_;
|
||||
ProtocolErrorType error_type_;
|
||||
uint32_t capture_;
|
||||
uint8_t clock_;
|
||||
uint32_t data_;
|
||||
uint8_t bit_pos_;
|
||||
int32_t timeout_counter_; // <0 no timeout
|
||||
|
||||
int32_t device_timeout_;
|
||||
|
||||
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||
bool init_esp32_timer_();
|
||||
void start_esp32_timer_(uint64_t alarm_value);
|
||||
#endif
|
||||
|
||||
void stop_timer_();
|
||||
|
||||
void read_(); // data detected start reading
|
||||
void start_read_timer_(); // reading timer_ to sample at 1/5 of manchester code bit length (at 5kHz)
|
||||
void start_write_timer_(); // writing timer_ to send manchester code (at 2kHz)
|
||||
bool check_parity_(uint32_t val);
|
||||
|
||||
void bit_read_(uint8_t value);
|
||||
ProtocolErrorType verify_stop_bit_(uint8_t value);
|
||||
void write_bit_(uint8_t high, uint8_t clock);
|
||||
|
||||
#ifdef ESP8266
|
||||
// ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm
|
||||
static OpenTherm *instance_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace opentherm
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,3 @@
|
||||
opentherm:
|
||||
in_pin: 1
|
||||
out_pin: 2
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user