[dsmr] Improve performance. Add missing sensors. Remove Crypto-no-arduino. (#15875)

This commit is contained in:
PolarGoose authored and GitHub committed 2026-04-22 13:49:14 -04:00
1 parent fcbc4d64fe
commit ea2e36e55a
10 files changed
+369 -349

No files matched your search

+1 -1
View File
@@ -1 +1 @@
c65f1a0804a7765462d570c50891ac719260592df2c9cdfe88233fc346ac59e9 256216e144a626c8c9d1a458920a9db3de7dfc8c6a1b44b87946b9752e81026c
+49 -16
View File
@@ -1,8 +1,19 @@
import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import uart from esphome.components import uart
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID from esphome.const import (
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_RX_BUFFER_SIZE,
CONF_UART_ID,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@glmnet", "@PolarGoose"] CODEOWNERS = ["@glmnet", "@PolarGoose"]
@@ -21,8 +32,7 @@ CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_INTERVAL = "request_interval"
CONF_REQUEST_PIN = "request_pin" CONF_REQUEST_PIN = "request_pin"
# Hack to prevent compile error due to ambiguity with lib namespace dsmr_ns = cg.esphome_ns.namespace("dsmr")
dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
@@ -54,24 +64,47 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
uart_component = await cg.get_variable(config[CONF_UART_ID]) uart_component = await cg.get_variable(config[CONF_UART_ID])
var = cg.new_Pvariable(config[CONF_ID], uart_component, config[CONF_CRC_CHECK])
cg.add(var.set_max_telegram_length(config[CONF_MAX_TELEGRAM_LENGTH]))
if CONF_DECRYPTION_KEY in config:
cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY]))
await cg.register_component(var, config)
if CONF_REQUEST_PIN in config: if CONF_REQUEST_PIN in config:
request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN]) request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN])
cg.add(var.set_request_pin(request_pin)) else:
cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) request_pin = cg.nullptr
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) decryption_key = config.get(CONF_DECRYPTION_KEY)
if decryption_key is None:
decryption_key = cg.nullptr
var = cg.new_Pvariable(
config[CONF_ID],
uart_component,
config[CONF_CRC_CHECK],
config[CONF_MAX_TELEGRAM_LENGTH],
config[CONF_REQUEST_INTERVAL].total_milliseconds,
config[CONF_RECEIVE_TIMEOUT].total_milliseconds,
request_pin,
decryption_key,
)
await cg.register_component(var, config)
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID])) cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
# DSMR Parser cg.add_library("esphome/dsmr_parser", "1.4.0")
cg.add_library("esphome/dsmr_parser", "1.1.0")
# Crypto
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0") def final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
for uart_conf in full_config["uart"]:
if uart_conf[CONF_ID] == config[CONF_UART_ID]:
rx_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE]
if rx_buffer_size < 1500:
_LOGGER.warning(
"UART '%s' rx_buffer_size should be bigger than 1500 bytes to avoid packet losses (currently %d bytes).",
config[CONF_UART_ID],
rx_buffer_size,
)
break
return config
FINAL_VALIDATE_SCHEMA = final_validate
+141 -266
View File
@@ -1,315 +1,183 @@
#include "dsmr.h" // Ignore Zephyr. It doesn't have any encryption library.
#include "esphome/core/helpers.h" #if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
#include "esphome/core/log.h"
#include <AES.h> #include "dsmr.h"
#include <Crypto.h> #include "esphome/core/log.h"
#include <GCM.h> #include <dsmr_parser/util.h>
namespace esphome::dsmr { namespace esphome::dsmr {
static const char *const TAG = "dsmr"; static constexpr auto &TAG = "dsmr";
static void log_callback(dsmr_parser::LogLevel level, const char *fmt, va_list args) {
std::array<char, 256> buf;
vsnprintf(buf.data(), buf.size(), fmt, args);
switch (level) {
case dsmr_parser::LogLevel::ERROR:
ESP_LOGE(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::WARNING:
ESP_LOGW(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::INFO:
ESP_LOGI(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::VERBOSE:
ESP_LOGV(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::VERY_VERBOSE:
ESP_LOGVV(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::DEBUG:
ESP_LOGD(TAG, "%s", buf.data());
break;
}
}
void Dsmr::setup() { void Dsmr::setup() {
this->telegram_ = new char[this->max_telegram_len_]; // NOLINT dsmr_parser::Logger::set_log_function(log_callback);
if (this->request_pin_ != nullptr) { if (this->request_pin_ != nullptr) {
this->request_pin_->setup(); this->request_pin_->setup();
} }
} }
void Dsmr::loop() { void Dsmr::loop() {
if (this->ready_to_request_data_()) { if (!this->ready_to_request_data_()) {
if (this->decryption_key_.empty()) { return;
this->receive_telegram_(); }
} else {
this->receive_encrypted_telegram_(); if (this->encryption_enabled_) {
} this->receive_encrypted_telegram_();
} else {
this->receive_telegram_();
} }
} }
bool Dsmr::ready_to_request_data_() { bool Dsmr::ready_to_request_data_() {
// When using a request pin, then wait for the next request interval. if (!this->requesting_data_ && this->request_interval_reached_()) {
if (this->request_pin_ != nullptr) { this->start_requesting_data_();
if (!this->requesting_data_ && this->request_interval_reached_()) {
this->start_requesting_data_();
}
}
// Otherwise, sink serial data until next request interval.
else {
if (this->request_interval_reached_()) {
this->start_requesting_data_();
}
if (!this->requesting_data_) {
this->drain_rx_buffer_();
}
} }
return this->requesting_data_; return this->requesting_data_;
} }
bool Dsmr::request_interval_reached_() { bool Dsmr::request_interval_reached_() const {
if (this->last_request_time_ == 0) { if (this->last_request_time_ == 0) {
return true; return true;
} }
return millis() - this->last_request_time_ > this->request_interval_; return millis() - this->last_request_time_ > this->request_interval_;
} }
bool Dsmr::receive_timeout_reached_() { return millis() - this->last_read_time_ > this->receive_timeout_; }
bool Dsmr::available_within_timeout_() {
// Data are available for reading on the UART bus?
// Then we can start reading right away.
if (this->available()) {
this->last_read_time_ = millis();
return true;
}
// When we're not in the process of reading a telegram, then there is
// no need to actively wait for new data to come in.
if (!header_found_) {
return false;
}
// A telegram is being read. The smart meter might not deliver a telegram
// in one go, but instead send it in chunks with small pauses in between.
// When the UART RX buffer cannot hold a full telegram, then make sure
// that the UART read buffer does not overflow while other components
// perform their work in their loop. Do this by not returning control to
// the main loop, until the read timeout is reached.
if (this->parent_->get_rx_buffer_size() < this->max_telegram_len_) {
while (!this->receive_timeout_reached_()) {
delay(5);
if (this->available()) {
this->last_read_time_ = millis();
return true;
}
}
}
// No new data has come in during the read timeout? Then stop reading the
// telegram and start waiting for the next one to arrive.
if (this->receive_timeout_reached_()) {
ESP_LOGW(TAG, "Timeout while reading data for telegram");
this->reset_telegram_();
}
return false;
}
void Dsmr::start_requesting_data_() { void Dsmr::start_requesting_data_() {
if (!this->requesting_data_) { if (this->requesting_data_) {
if (this->request_pin_ != nullptr) { return;
ESP_LOGV(TAG, "Start requesting data from P1 port");
this->request_pin_->digital_write(true);
} else {
ESP_LOGV(TAG, "Start reading data from P1 port");
}
this->requesting_data_ = true;
this->last_request_time_ = millis();
} }
ESP_LOGV(TAG, "Start reading data from P1 port");
this->flush_rx_buffer_();
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Set request pin to 1");
this->request_pin_->digital_write(true);
}
this->requesting_data_ = true;
this->last_request_time_ = millis();
} }
void Dsmr::stop_requesting_data_() { void Dsmr::stop_requesting_data_() {
if (this->requesting_data_) { if (!this->requesting_data_) {
if (this->request_pin_ != nullptr) { return;
ESP_LOGV(TAG, "Stop requesting data from P1 port");
this->request_pin_->digital_write(false);
} else {
ESP_LOGV(TAG, "Stop reading data from P1 port");
}
this->drain_rx_buffer_();
this->requesting_data_ = false;
} }
ESP_LOGV(TAG, "Stop reading data from P1 port");
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Set request pin to 0");
this->request_pin_->digital_write(false);
}
this->requesting_data_ = false;
} }
void Dsmr::drain_rx_buffer_() { void Dsmr::flush_rx_buffer_() {
uint8_t buf[64]; ESP_LOGV(TAG, "Flush UART RX buffer");
size_t avail; while (!this->uart_read_chunk_().empty()) {
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
break;
}
} }
} }
void Dsmr::reset_telegram_() {
this->header_found_ = false;
this->footer_found_ = false;
this->bytes_read_ = 0;
this->crypt_bytes_read_ = 0;
this->crypt_telegram_len_ = 0;
}
void Dsmr::receive_telegram_() { void Dsmr::receive_telegram_() {
while (this->available_within_timeout_()) { for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) {
// Read all available bytes in batches to reduce UART call overhead. for (uint8_t byte : data) {
uint8_t buf[64]; const auto telegram = this->packet_accumulator_.process_byte(byte);
size_t avail = this->available(); if (!telegram) { // No full packet received yet
while (avail > 0) { continue;
size_t to_read = std::min(avail, sizeof(buf)); }
if (!this->read_array(buf, to_read)) if (this->parse_telegram_(telegram.value())) {
return; return;
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram header, i.e. forward slash.
if (c == '/') {
ESP_LOGV(TAG, "Header of telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
if (!this->header_found_)
continue;
// Check for buffer overflow.
if (this->bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Some v2.2 or v3 meters will send a new value which starts with '('
// in a new line, while the value belongs to the previous ObisId. For
// proper parsing, remove these new line characters.
if (c == '(') {
while (true) {
auto previous_char = this->telegram_[this->bytes_read_ - 1];
if (previous_char == '\n' || previous_char == '\r') {
this->bytes_read_--;
} else {
break;
}
}
}
// Store the byte in the buffer.
this->telegram_[this->bytes_read_] = c;
this->bytes_read_++;
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
if (c == '!') {
ESP_LOGV(TAG, "Footer of telegram found");
this->footer_found_ = true;
continue;
}
// Check for the end of the hex checksum, i.e. a newline.
if (this->footer_found_ && c == '\n') {
// Parse the telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
}
} }
} }
} }
} }
void Dsmr::receive_encrypted_telegram_() { void Dsmr::receive_encrypted_telegram_() {
while (this->available_within_timeout_()) { for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) {
// Read all available bytes in batches to reduce UART call overhead. for (uint8_t byte : data) {
uint8_t buf[64]; if (this->buffer_pos_ >= this->buffer_.size()) { // Reset buffer if overflow
size_t avail = this->available(); ESP_LOGW(TAG, "Encrypted buffer overflow, resetting");
while (avail > 0) { this->buffer_pos_ = 0;
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram start byte.
if (!this->header_found_) {
if ((uint8_t) c != 0xDB) {
continue;
}
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
// Check for buffer overflow.
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Store the byte in the buffer.
this->crypt_telegram_[this->crypt_bytes_read_] = c;
this->crypt_bytes_read_++;
// Read the length of the incoming encrypted telegram.
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
// Complete header + data bytes
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
}
// Check for the end of the encrypted telegram.
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
continue;
}
ESP_LOGV(TAG, "End of encrypted telegram found");
// Decrypt the encrypted telegram.
GCM<AES128> *gcmaes128{new GCM<AES128>()};
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
// the iv is 8 bytes of the system title + 4 bytes frame counter
// system title is at byte 2 and frame counter at byte 15
for (int i = 10; i < 14; i++)
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
constexpr uint16_t iv_size{12};
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
// the ciphertext start at byte 18
&this->crypt_telegram_[18],
// cipher size
this->crypt_bytes_read_ - 17);
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
// Parse the decrypted telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
} }
this->buffer_[this->buffer_pos_] = byte;
this->buffer_pos_++;
} }
this->last_read_time_ = millis();
}
// Detect inter-frame delay. If no byte is received for more than receive_timeout, then the packet is complete.
if (millis() - this->last_read_time_ > this->receive_timeout_ && this->buffer_pos_ > 0) {
ESP_LOGV(TAG, "Encrypted telegram received (%zu bytes)", this->buffer_pos_);
const auto telegram = this->dlms_decryptor_.decrypt_inplace({this->buffer_.data(), this->buffer_pos_});
// Reset buffer position for the next packet
this->buffer_pos_ = 0;
this->last_read_time_ = 0;
if (!telegram) { // decryption failed
return;
}
// Parse and publish the telegram
this->parse_telegram_(telegram.value());
} }
} }
bool Dsmr::parse_telegram() { bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) {
MyData data;
ESP_LOGV(TAG, "Trying to parse telegram");
this->stop_requesting_data_(); this->stop_requesting_data_();
const auto &res = dsmr_parser::P1Parser::parse( ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.content().size());
data, this->telegram_, this->bytes_read_, false, ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.content().size()), telegram.content().data());
this->crc_check_); // Parse telegram according to data definition. Ignore unknown values.
if (res.err) {
// Parsing error, show it
auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_);
ESP_LOGE(TAG, "%s", err_str.c_str());
return false;
} else {
this->status_clear_warning();
this->publish_sensors(data);
// publish the telegram, after publishing the sensors so it can also trigger action based on latest values MyData data;
if (this->s_telegram_ != nullptr) { if (const bool res = dsmr_parser::DsmrParser::parse(data, telegram); !res) {
this->s_telegram_->publish_state(this->telegram_, this->bytes_read_); ESP_LOGE(TAG, "Failed to parse telegram");
} return false;
return true;
} }
this->status_clear_warning();
this->publish_sensors(data);
// Publish the telegram, after publishing the sensors so it can also trigger action based on latest values
if (this->s_telegram_ != nullptr) {
this->s_telegram_->publish_state(telegram.content().data(), telegram.content().size());
}
return true;
} }
void Dsmr::dump_config() { void Dsmr::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"DSMR:\n" "DSMR:\n"
" Max telegram length: %d\n" " Max telegram length: %zu\n"
" Receive timeout: %.1fs", " Receive timeout: %.1fs",
this->max_telegram_len_, this->receive_timeout_ / 1e3f); this->buffer_.size(), this->receive_timeout_ / 1e3f);
if (this->request_pin_ != nullptr) { if (this->request_pin_ != nullptr) {
LOG_PIN(" Request Pin: ", this->request_pin_); LOG_PIN(" Request Pin: ", this->request_pin_);
} }
@@ -324,30 +192,37 @@ void Dsmr::dump_config() {
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
} }
void Dsmr::set_decryption_key(const char *decryption_key) { void Dsmr::set_decryption_key_(const char *decryption_key) {
if (decryption_key == nullptr || decryption_key[0] == '\0') { if (decryption_key == nullptr || decryption_key[0] == '\0') {
ESP_LOGI(TAG, "Disabling decryption"); this->encryption_enabled_ = false;
this->decryption_key_.clear();
if (this->crypt_telegram_ != nullptr) {
delete[] this->crypt_telegram_;
this->crypt_telegram_ = nullptr;
}
return; return;
} }
if (!parse_hex(decryption_key, this->decryption_key_, 16)) { auto key = dsmr_parser::Aes128GcmDecryptionKey::from_hex(decryption_key);
ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters"); if (!key) {
this->decryption_key_.clear(); ESP_LOGE(TAG, "Error, decryption key has incorrect format");
this->encryption_enabled_ = false;
return; return;
} }
ESP_LOGI(TAG, "Decryption key is set"); ESP_LOGI(TAG, "Decryption key is set");
// Verbose level prints decryption key
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key);
if (this->crypt_telegram_ == nullptr) { this->gcm_decryptor_.set_encryption_key(key.value());
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT this->encryption_enabled_ = true;
}
std::span<uint8_t> Dsmr::uart_read_chunk_() {
const auto avail = this->available();
if (avail == 0) {
return {};
} }
size_t to_read = std::min(avail, uart_chunk_reading_buf_.size());
if (!this->read_array(uart_chunk_reading_buf_.data(), to_read)) {
return {};
}
return {uart_chunk_reading_buf_.data(), to_read};
} }
} // namespace esphome::dsmr } // namespace esphome::dsmr
#endif
+65 -64
View File
@@ -1,31 +1,41 @@
#pragma once #pragma once
// Ignore Zephyr. It doesn't have any encryption library.
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <dsmr_parser/dlms_packet_decryptor.h>
#include <dsmr_parser/fields.h> #include <dsmr_parser/fields.h>
#include <dsmr_parser/packet_accumulator.h>
#include <dsmr_parser/parser.h> #include <dsmr_parser/parser.h>
#include <array>
#include <span>
#include <vector> #include <vector>
#if __has_include(<psa/crypto.h>)
#include <dsmr_parser/decryption/aes128gcm_tfpsa.h>
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa;
#elif __has_include(<mbedtls/gcm.h>)
#if __has_include(<mbedtls/esp_config.h>)
#include <mbedtls/esp_config.h>
#endif
#include <dsmr_parser/decryption/aes128gcm_mbedtls.h>
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls;
#elif __has_include(<bearssl/bearssl.h>)
#include <dsmr_parser/decryption/aes128gcm_bearssl.h>
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl;
#else
#error "The platform doesn't provide a compatible encryption library for dsmr_parser"
#endif
namespace esphome::dsmr { namespace esphome::dsmr {
using namespace dsmr_parser::fields; using namespace dsmr_parser::fields;
// DSMR_**_LIST generated by ESPHome and written in esphome/core/defines
#if !defined(DSMR_SENSOR_LIST) && !defined(DSMR_TEXT_SENSOR_LIST)
// Neither set, set it to a dummy value to not break build
#define DSMR_TEXT_SENSOR_LIST(F, SEP) F(identification)
#endif
#if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST)
#define DSMR_BOTH ,
#else
#define DSMR_BOTH
#endif
#ifndef DSMR_SENSOR_LIST #ifndef DSMR_SENSOR_LIST
#define DSMR_SENSOR_LIST(F, SEP) #define DSMR_SENSOR_LIST(F, SEP)
#endif #endif
@@ -34,21 +44,33 @@ using namespace dsmr_parser::fields;
#define DSMR_TEXT_SENSOR_LIST(F, SEP) #define DSMR_TEXT_SENSOR_LIST(F, SEP)
#endif #endif
#define DSMR_DATA_SENSOR(s) s #define DSMR_IDENTITY(s) s
#define DSMR_COMMA , #define DSMR_COMMA ,
#define DSMR_PREPEND_COMMA(...) __VA_OPT__(, ) __VA_ARGS__
using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA) #ifdef DSMR_TEXT_SENSOR_LIST_DEFINED
DSMR_BOTH DSMR_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA)>; using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA)
DSMR_PREPEND_COMMA(DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA))>;
#else
using MyData = dsmr_parser::ParsedData<DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA)>;
#endif
class Dsmr : public Component, public uart::UARTDevice { class Dsmr : public Component, public uart::UARTDevice {
public: public:
Dsmr(uart::UARTComponent *uart, bool crc_check) : uart::UARTDevice(uart), crc_check_(crc_check) {} Dsmr(uart::UARTComponent *uart, bool crc_check, size_t max_telegram_length, uint32_t request_interval,
uint32_t receive_timeout, GPIOPin *request_pin, const char *decryption_key)
: uart::UARTDevice(uart),
request_interval_(request_interval),
receive_timeout_(receive_timeout),
request_pin_(request_pin),
buffer_(max_telegram_length),
packet_accumulator_(buffer_, crc_check) {
this->set_decryption_key_(decryption_key);
}
void setup() override; void setup() override;
void loop() override; void loop() override;
bool parse_telegram();
void publish_sensors(MyData &data) { void publish_sensors(MyData &data) {
#define DSMR_PUBLISH_SENSOR(s) \ #define DSMR_PUBLISH_SENSOR(s) \
if (data.s##_present && this->s_##s##_ != nullptr) \ if (data.s##_present && this->s_##s##_ != nullptr) \
@@ -57,20 +79,15 @@ class Dsmr : public Component, public uart::UARTDevice {
#define DSMR_PUBLISH_TEXT_SENSOR(s) \ #define DSMR_PUBLISH_TEXT_SENSOR(s) \
if (data.s##_present && this->s_##s##_ != nullptr) \ if (data.s##_present && this->s_##s##_ != nullptr) \
s_##s##_->publish_state(data.s.c_str()); s_##s##_->publish_state(data.s.data(), data.s.size());
DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, ) DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, )
}; };
void dump_config() override; void dump_config() override;
void set_decryption_key(const char *decryption_key);
// Remove before 2026.8.0 // Remove before 2026.8.0
ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") ESPDEPRECATED("Use 'decryption_key' configuration parameter. This method will be removed in 2026.8.0", "2026.2.0")
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key_(decryption_key.c_str()); }
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }
void set_receive_timeout(uint32_t timeout) { this->receive_timeout_ = timeout; }
// Sensor setters // Sensor setters
#define DSMR_SET_SENSOR(s) \ #define DSMR_SET_SENSOR(s) \
@@ -85,56 +102,40 @@ class Dsmr : public Component, public uart::UARTDevice {
void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; } void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; }
protected: protected:
void set_decryption_key_(const char *decryption_key);
void receive_telegram_(); void receive_telegram_();
void receive_encrypted_telegram_(); void receive_encrypted_telegram_();
void reset_telegram_(); void flush_rx_buffer_();
void drain_rx_buffer_();
/// Wait for UART data to become available within the read timeout. bool parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram);
/// bool request_interval_reached_() const;
/// The smart meter might provide data in chunks, causing available() to
/// return 0. When we're already reading a telegram, then we don't return
/// right away (to handle further data in an upcoming loop) but wait a
/// little while using this method to see if more data are incoming.
/// By not returning, we prevent other components from taking so much
/// time that the UART RX buffer overflows and bytes of the telegram get
/// lost in the process.
bool available_within_timeout_();
// Request telegram
uint32_t request_interval_;
bool request_interval_reached_();
GPIOPin *request_pin_{nullptr};
uint32_t last_request_time_{0};
bool requesting_data_{false};
bool ready_to_request_data_(); bool ready_to_request_data_();
void start_requesting_data_(); void start_requesting_data_();
void stop_requesting_data_(); void stop_requesting_data_();
std::span<uint8_t> uart_read_chunk_();
// Read telegram // Config
uint32_t request_interval_;
uint32_t receive_timeout_; uint32_t receive_timeout_;
bool receive_timeout_reached_(); GPIOPin *request_pin_{nullptr};
size_t max_telegram_len_;
char *telegram_{nullptr};
size_t bytes_read_{0};
uint8_t *crypt_telegram_{nullptr};
size_t crypt_telegram_len_{0};
size_t crypt_bytes_read_{0};
uint32_t last_read_time_{0};
bool header_found_{false};
bool footer_found_{false};
// handled outside dsmr
text_sensor::TextSensor *s_telegram_{nullptr}; text_sensor::TextSensor *s_telegram_{nullptr};
// Sensor member pointers
#define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; #define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr};
DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, )
#define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr}; #define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr};
DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, ) DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, )
std::vector<uint8_t> decryption_key_{}; // State
bool crc_check_; uint32_t last_request_time_{0};
uint32_t last_read_time_{0};
bool requesting_data_{false};
bool encryption_enabled_{false};
size_t buffer_pos_{0};
std::vector<uint8_t> buffer_;
dsmr_parser::PacketAccumulator packet_accumulator_;
Aes128GcmDecryptorImpl gcm_decryptor_;
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_{gcm_decryptor_};
std::array<uint8_t, 256> uart_chunk_reading_buf_;
}; };
} // namespace esphome::dsmr } // namespace esphome::dsmr
#endif
+81
View File
@@ -10,6 +10,7 @@ from esphome.const import (
DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS, DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_WATER, DEVICE_CLASS_WATER,
@@ -119,6 +120,42 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_ENERGY, device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING, state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
cv.Optional("energy_delivered_tariff1_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff2_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff3_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff1_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff2_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff3_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("total_imported_energy"): sensor.sensor_schema( cv.Optional("total_imported_energy"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3, accuracy_decimals=3,
@@ -511,6 +548,12 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_GAS, device_class=DEVICE_CLASS_GAS,
state_class=STATE_CLASS_TOTAL_INCREASING, state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
cv.Optional("gas_delivered_gj"): sensor.sensor_schema(
unit_of_measurement=UNIT_GIGA_JOULE,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("water_delivered"): sensor.sensor_schema( cv.Optional("water_delivered"): sensor.sensor_schema(
unit_of_measurement=UNIT_CUBIC_METER, unit_of_measurement=UNIT_CUBIC_METER,
accuracy_decimals=3, accuracy_decimals=3,
@@ -614,6 +657,12 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("active_demand_net"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_demand_abs"): sensor.sensor_schema( cv.Optional("active_demand_abs"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT, unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3, accuracy_decimals=3,
@@ -728,6 +777,37 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l1"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l2"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l3"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("min_power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("period_3_for_instantaneous_values"): sensor.sensor_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@@ -746,6 +826,7 @@ async def to_code(config):
sensors.append(f"F({key})") sensors.append(f"F({key})")
if sensors: if sensors:
cg.add_define("DSMR_SENSOR_LIST_DEFINED")
cg.add_define( cg.add_define(
"DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors)) "DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
) )
+3
View File
@@ -15,7 +15,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(), cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(), cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(), cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff_il"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(), cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_failure_log_il"): text_sensor.text_sensor_schema(),
cv.Optional("message_short"): text_sensor.text_sensor_schema(), cv.Optional("message_short"): text_sensor.text_sensor_schema(),
cv.Optional("message_long"): text_sensor.text_sensor_schema(), cv.Optional("message_long"): text_sensor.text_sensor_schema(),
cv.Optional("equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("equipment_id"): text_sensor.text_sensor_schema(),
@@ -52,6 +54,7 @@ async def to_code(config):
text_sensors.append(f"F({key})") text_sensors.append(f"F({key})")
if text_sensors: if text_sensors:
cg.add_define("DSMR_TEXT_SENSOR_LIST_DEFINED")
cg.add_define( cg.add_define(
"DSMR_TEXT_SENSOR_LIST(F, sep)", "DSMR_TEXT_SENSOR_LIST(F, sep)",
cg.RawExpression(" sep ".join(text_sensors)), cg.RawExpression(" sep ".join(text_sensors)),
+1 -2
View File
@@ -37,8 +37,7 @@ lib_deps_base =
wjtje/qr-code-generator-library@1.7.0 ; qr_code wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier pavlodn/HaierProtocol@0.9.31 ; haier
esphome/dsmr_parser@1.1.0 ; dsmr esphome/dsmr_parser@1.4.0 ; dsmr
polargoose/Crypto-no-arduino@0.4.0 ; dsmr
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
; This is using the repository until a new release is published to PlatformIO ; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
@@ -5,3 +5,10 @@ packages:
uart: !include ../../test_build_components/common/uart/esp32-ard.yaml uart: !include ../../test_build_components/common/uart/esp32-ard.yaml
<<: !include common.yaml <<: !include common.yaml
sensor:
- platform: dsmr
energy_delivered_lux:
name: "Energy Consumed Luxembourg. OBIS: 1-0:1.8.0"
energy_delivered_tariff1:
name: "Energy Consumed Tariff 1. OBIS: 1-0:1.8.1"
+14
View File
@@ -5,3 +5,17 @@ packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
<<: !include common.yaml <<: !include common.yaml
sensor:
- platform: dsmr
energy_delivered_lux:
name: "Energy Consumed Luxembourg. OBIS: 1-0:1.8.0"
energy_delivered_tariff1:
name: "Energy Consumed Tariff 1. OBIS: 1-0:1.8.1"
text_sensor:
- platform: dsmr
identification:
name: "DSMR Identification"
p1_version:
name: "DSMR Version. OBIS: 1-3:0.2.8"
@@ -5,3 +5,10 @@ packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
<<: !include common.yaml <<: !include common.yaml
text_sensor:
- platform: dsmr
identification:
name: "DSMR Identification"
p1_version:
name: "DSMR Version. OBIS: 1-3:0.2.8"