mirror of
https://github.com/esphome/esphome.git
synced 2024-12-31 18:07:48 +01:00
Initial component for DUCO ventilation
This component is based on Modbus. The underlying protocol used by DUCO has some similarities with Modbus, however it does have some differences as well. Therefor a custom component is needed.
This commit is contained in:
parent
c457d8835e
commit
68c914ef1f
44
esphome/components/duco/__init__.py
Normal file
44
esphome/components/duco/__init__.py
Normal file
@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DISABLE_CRC, CONF_ID
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
duco_ns = cg.esphome_ns.namespace("duco")
|
||||
Duco = duco_ns.class_("Duco", cg.Component, uart.UARTDevice)
|
||||
# ModbusDevice = duco_ns.class_("ModbusDevice")
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_DUCO_ID = "duco_id"
|
||||
CONF_SEND_WAIT_TIME = "send_wait_time"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Duco),
|
||||
cv.Optional(
|
||||
CONF_SEND_WAIT_TIME, default="250ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
# A schema for components like sensors
|
||||
DUCO_COMPONENT_SCHEMA = cv.Schema({cv.GenerateID(CONF_DUCO_ID): cv.use_id(Duco)})
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_global(duco_ns.using)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME]))
|
||||
cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
|
196
esphome/components/duco/duco.cpp
Normal file
196
esphome/components/duco/duco.cpp
Normal file
@ -0,0 +1,196 @@
|
||||
#include "duco.h"
|
||||
#include "sensor/sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace duco {
|
||||
|
||||
static const char *const TAG = "duco";
|
||||
|
||||
void Duco::setup() {
|
||||
// no setup needed
|
||||
}
|
||||
void Duco::loop() {
|
||||
const uint32_t now = millis();
|
||||
|
||||
if (now - this->last_byte_ > 50) {
|
||||
// first try to finalize last message, before clearing the buffer
|
||||
this->finalize_message_();
|
||||
|
||||
this->rx_buffer_.clear();
|
||||
this->last_byte_ = now;
|
||||
}
|
||||
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
if (this->parse_byte_(byte)) {
|
||||
this->last_byte_ = now;
|
||||
} else {
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Duco::parse_byte_(uint8_t byte) {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
this->rx_buffer_.push_back(byte);
|
||||
const uint8_t *raw = &this->rx_buffer_[0];
|
||||
ESP_LOGV(TAG, "DUCO received Byte %d (0X%x)", byte, byte);
|
||||
// Byte 0: Packet length (match all)
|
||||
if (at == 0)
|
||||
return true;
|
||||
|
||||
// Byte 1: check if AA:55 header
|
||||
if (at == 1 && raw[0] == 0xAA && raw[1] == 0x55) {
|
||||
// ignore the AA:55 header
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte 1: function code
|
||||
if (at == 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (at >= 2 && this->rx_buffer_[at - 1] == 0xAA && this->rx_buffer_[at] == 0x01 && !this->removed_last_) {
|
||||
// The last byte was 0xAA, but the current one is 0x01, which means we ignore it (only once!)
|
||||
// This is an edge case in processing, likely to prevent accidentally introducing AA:55 in a message
|
||||
this->rx_buffer_.pop_back();
|
||||
this->removed_last_ = true;
|
||||
return true;
|
||||
}
|
||||
this->removed_last_ = false;
|
||||
|
||||
// Byte 2: Identification code
|
||||
if (at == 2)
|
||||
return true;
|
||||
|
||||
// finalize message when AA:55 is found
|
||||
if (at >= 2 && this->rx_buffer_[at - 1] == 0xAA && this->rx_buffer_[at] == 0x55) {
|
||||
this->rx_buffer_.pop_back();
|
||||
this->rx_buffer_.pop_back();
|
||||
at--;
|
||||
at--;
|
||||
|
||||
this->finalize_message_();
|
||||
|
||||
// reset buffer after finalizing
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Duco::finalize_message_() {
|
||||
if (this->rx_buffer_.size() == 0)
|
||||
return;
|
||||
const uint8_t *raw = &this->rx_buffer_[0];
|
||||
|
||||
uint8_t data_len = raw[0];
|
||||
|
||||
// Byte data_offset+len+1: CRC_HI (over all bytes)
|
||||
uint16_t computed_crc = crc16(raw, data_len + 1);
|
||||
uint16_t remote_crc = uint16_t(raw[data_len + 1]) | (uint16_t(raw[data_len + 2]) << 8);
|
||||
if (computed_crc != remote_crc) {
|
||||
if (this->disable_crc_) {
|
||||
ESP_LOGD(TAG, "CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> data(this->rx_buffer_.begin(), this->rx_buffer_.begin() + data_len + 1);
|
||||
|
||||
// see if the response exists for waiting
|
||||
auto it = waiting_for_response.find(data[2]);
|
||||
if (it != waiting_for_response.end()) {
|
||||
waiting_for_response[data[2]]->receive_response(data);
|
||||
}
|
||||
}
|
||||
|
||||
void Duco::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DUCO:");
|
||||
ESP_LOGCONFIG(TAG, " Send Wait Time: %d ms", this->send_wait_time_);
|
||||
ESP_LOGCONFIG(TAG, " CRC Disabled: %s", YESNO(this->disable_crc_));
|
||||
}
|
||||
|
||||
float Duco::get_setup_priority() const {
|
||||
// After UART bus
|
||||
return setup_priority::BUS - 1.0f;
|
||||
}
|
||||
|
||||
uint8_t Duco::next_id_() {
|
||||
last_id_++;
|
||||
if (last_id_ == 0xAA or last_id_ == 0x55) {
|
||||
return next_id_();
|
||||
}
|
||||
return last_id_;
|
||||
}
|
||||
|
||||
void Duco::stop_waiting(uint8_t message_id) {
|
||||
auto it = waiting_for_response.find(message_id);
|
||||
|
||||
if (it != waiting_for_response.end()) {
|
||||
waiting_for_response.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Duco::send(uint8_t function, std::vector<uint8_t> message, DucoDevice *device) {
|
||||
std::vector<uint8_t> data;
|
||||
uint8_t message_id = this->next_id_();
|
||||
|
||||
data.push_back(message.size() + 2);
|
||||
data.push_back(function);
|
||||
data.push_back(message_id);
|
||||
data.insert(data.end(), message.begin(), message.end());
|
||||
|
||||
send_raw(data);
|
||||
|
||||
this->waiting_for_response[message_id] = device;
|
||||
}
|
||||
|
||||
// Helper function for lambdas
|
||||
// Send raw command. Except CRC everything must be contained in payload
|
||||
void Duco::send_raw(const std::vector<uint8_t> &payload) {
|
||||
if (payload.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto crc = crc16(payload.data(), payload.size());
|
||||
|
||||
this->write_byte(0xAA);
|
||||
this->write_byte(0x55);
|
||||
this->write_array(payload);
|
||||
this->write_byte(crc & 0xFF);
|
||||
this->write_byte((crc >> 8) & 0xFF);
|
||||
this->flush();
|
||||
|
||||
ESP_LOGD(TAG, "DUCO write: %s", format_hex_pretty(payload).c_str());
|
||||
last_send_ = millis();
|
||||
}
|
||||
|
||||
void Duco::debug_hex(std::vector<uint8_t> bytes, uint8_t separator) {
|
||||
std::string res;
|
||||
res += "DUCO msg: ";
|
||||
|
||||
size_t len = bytes.size();
|
||||
|
||||
char buf[5];
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (i > 0) {
|
||||
res += separator;
|
||||
}
|
||||
sprintf(buf, "%02X", bytes[i]);
|
||||
res += buf;
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", res.c_str());
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void Duco::add_sensor_item(DucoDevice *sensor) { sensor->set_parent(this); }
|
||||
|
||||
} // namespace duco
|
||||
} // namespace esphome
|
59
esphome/components/duco/duco.h
Normal file
59
esphome/components/duco/duco.h
Normal file
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
namespace esphome {
|
||||
namespace duco {
|
||||
|
||||
class DucoDevice;
|
||||
|
||||
class Duco : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
Duco() { this->last_id_ = 10; };
|
||||
|
||||
void setup() override;
|
||||
|
||||
void loop() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void send(uint8_t function, std::vector<uint8_t> message, DucoDevice *device);
|
||||
void send_raw(const std::vector<uint8_t> &payload);
|
||||
void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; }
|
||||
void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
|
||||
|
||||
void add_sensor_item(DucoDevice *sensor);
|
||||
|
||||
std::map<uint8_t, DucoDevice *> waiting_for_response;
|
||||
void stop_waiting(uint8_t message_id);
|
||||
|
||||
protected:
|
||||
uint8_t last_id_ = 0;
|
||||
uint8_t next_id_();
|
||||
|
||||
bool parse_byte_(uint8_t byte);
|
||||
void finalize_message_();
|
||||
uint16_t send_wait_time_{250};
|
||||
bool disable_crc_;
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
uint32_t last_byte_{0};
|
||||
uint32_t last_send_{0};
|
||||
bool removed_last_ = false;
|
||||
|
||||
void debug_hex(std::vector<uint8_t> bytes, uint8_t separator);
|
||||
};
|
||||
|
||||
class DucoDevice : public Parented<Duco> {
|
||||
public:
|
||||
virtual void receive_response(std::vector<uint8_t> message) {}
|
||||
};
|
||||
|
||||
} // namespace duco
|
||||
} // namespace esphome
|
30
esphome/components/duco/sensor/__init__.py
Normal file
30
esphome/components/duco/sensor/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
from .. import CONF_DUCO_ID, DUCO_COMPONENT_SCHEMA
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
duco_ns = cg.esphome_ns.namespace("duco")
|
||||
DucoSensor = duco_ns.class_("DucoSensor", cg.PollingComponent, sensor.Sensor)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(DucoSensor)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(DUCO_COMPONENT_SCHEMA)
|
||||
.extend({cv.GenerateID(): cv.declare_id(DucoSensor)})
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
parent = await cg.get_variable(config[CONF_DUCO_ID])
|
||||
cg.add(parent.add_sensor_item(var))
|
35
esphome/components/duco/sensor/sensor.cpp
Normal file
35
esphome/components/duco/sensor/sensor.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#include "sensor.h"
|
||||
#include "../duco.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace duco {
|
||||
|
||||
static const char *const TAG = "duco sensor";
|
||||
|
||||
void DucoSensor::setup() {}
|
||||
|
||||
void DucoSensor::update() {
|
||||
// ask for serial
|
||||
std::vector<uint8_t> message = {0x01, 0x01, 0x00, 0x1a, 0x10};
|
||||
this->parent_->send(0x10, message, this);
|
||||
}
|
||||
|
||||
float DucoSensor::get_setup_priority() const {
|
||||
// After DUCO
|
||||
return setup_priority::BUS - 2.0f;
|
||||
}
|
||||
|
||||
void DucoSensor::receive_response(std::vector<uint8_t> message) {
|
||||
if (message[1] == 0x12) {
|
||||
// Serial response received, parse it
|
||||
std::string serial(message.begin() + 5, message.end());
|
||||
ESP_LOGD(TAG, "Box Serial: %s", serial.c_str());
|
||||
|
||||
// do not wait for new messages with the same ID
|
||||
this->parent_->stop_waiting(message[2]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace duco
|
||||
} // namespace esphome
|
25
esphome/components/duco/sensor/sensor.h
Normal file
25
esphome/components/duco/sensor/sensor.h
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace duco {
|
||||
|
||||
class DucoSensor : public DucoDevice, public PollingComponent, public sensor::Sensor {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void receive_response(std::vector<uint8_t> message) override;
|
||||
|
||||
protected:
|
||||
uint8_t attempts = 0;
|
||||
};
|
||||
|
||||
} // namespace duco
|
||||
} // namespace esphome
|
Loading…
Reference in New Issue
Block a user