Compare commits

..

2 Commits

Author SHA1 Message Date
Tomer27cz 81bbd19c09 Enable UART pull-up
Add explicit pull-up configuration for the UART RX pin and disable the logger (baud_rate: 0) to avoid pin conflicts with an RS485 converter. Updates applied to esphome-smartmeter.yaml and both README (EN/CZ) files with notes explaining that esp-idf > 5.x defaults pins to floating so RX needs pull-up, and that the logger was turned off because the same pins are shared with the RS485 converter.
2026-03-07 13:44:06 +01:00
Tomer27cz 1db8e48acf new dlms_push component
dlms_push is a complete rewrite of the component using common esphome structure. The new component no longer uses Gurux library so it is significantly smaller and easier to maintain.

Update README (EN/CZ) to document dlms_push, replace xt211 examples, and expose new config options (show_log, receive_timeout, custom_pattern).

Also update esphome-smartmeter.yaml accordingly.
2026-03-07 13:06:16 +01:00
11 changed files with 1347 additions and 80 deletions

View File

@ -138,11 +138,11 @@ Power Led (volitelné):
# Custom ESPHome komponenta # Custom ESPHome komponenta
Pro čtení PUSH zpráv DLMS/Cosem z XT211 jsem upravil existující projekt [esphome-dlms-cosem](https://github.com/latonita/esphome-dlms-cosem). Existují dvě verze komponenty v tomto repozitáři:
- `xt211` - původní verze, kterou jsem použil pro svůj setup, založená na [esphome-dlms-cosem](https://github.com/latonita/esphome-dlms-cosem&#41)
- `dlms_push` - napsaná od začátku (už nepoužívá Gurux knihovny) se strukturou esphome komponentů, je flexibilnější a snazší na údržbu.
Odstranil jsem polling, opravil pár chyb a přidal podporu pro binární senzory. - Používám a budu udržovat verzi `dlms_push`, ale verze `xt211` je stále k dispozici pro referenci a pro ty, kteří ji chtějí použít tak, jak je.
Více detailů v projektu [esphome-dlms-cosem repository](https://github.com/latonita/esphome-dlms-cosem) od [latonita](https://github.com/latonita)
## ESPHome konfigurace ## ESPHome konfigurace
@ -160,34 +160,57 @@ Přidej externí komponentu:
```yaml ```yaml
external_components: external_components:
- source: github://Tomer27cz/xt211 - source: github://Tomer27cz/xt211
components: [xt211] components: [dlms_push]
refresh: 1s refresh: 1s
``` ```
Poté nastav DLMS/Cosem komponentu:
- push_show_log: true (volitelné, bude zobrazovat surové PUSH zprávy v logu pro debug a testování)
Až to bude fungovat, logy vypni. [//]: # (The configuration options are:)
[//]: # (- `show_log` (optional, default: `false`) - whether to show the log of the DLMS/COSEM communication. This is useful for debugging and first setup, but it can be quite verbose.)
[//]: # (- `receive_timeout` (optional, default: `50ms`) - the timeout for receiving data from the meter. If the meter does not send any data within this time, the communication is considered finished and it will be processed.)
[//]: # (- `custom_pattern` (optional) - custom COSEM object pattern)
Konfigurační možnosti jsou:
- `show_log` (volitelné, výchozí: `false`) - zda zobrazit log komunikace DLMS/COSEM. To je užitečné pro ladění a první nastavení, ale může být docela obsáhlé.
- `receive_timeout` (volitelné, výchozí: `50ms`) - časový limit pro přijímání dat z elektroměru. Pokud elektroměr během této doby nepošle žádná data, komunikace se považuje za ukončenou a bude zpracována.
- `custom_pattern` (volitelné) - vlastní vzor COSEM objektů
Až bude komponenta fungovat, můžeš nastavit `show_log` na `false`, aby se logy přestaly zobrazovat.
```yaml ```yaml
logger:
baud_rate: 0
uart: uart:
id: bus_1 id: bus_1
rx_pin:
number: GPIO21
mode:
input: true
pullup: true
tx_pin: GPIO20 tx_pin: GPIO20
rx_pin: GPIO21
baud_rate: 9600 baud_rate: 9600
data_bits: 8 data_bits: 8
parity: NONE parity: NONE
stop_bits: 1 stop_bits: 1
xt211: dlms_push:
push_show_log: true id: my_dlms_meter
uart_id: bus_1
``` ```
V novějších verzích ESPHome (esp-idf > 5.x) je potřeba pro piny `uart` nastavit pull-up, jinak komunikace nebude fungovat. Proto6e výchozí stav pinů byl změněn na "floating".
Používám stejné piny pro RS485 převodník jako pro logger, takže jsem musel logger vypnout nastavením `baud_rate` na 0. Pokud chceš logger ponechat povolený, můžeš použít jiné piny pro logger.
### Number sensor (`sensor`) ### Number sensor (`sensor`)
Moje spotřeba elektřiny se měří v kWh, ale elektroměr odesílá hodnotu ve Wh. Proto používám lambda filtr k převodu hodnoty z Wh na kWh vydělením 1000. Moje spotřeba elektřiny se měří v kWh, ale elektroměr odesílá hodnotu ve Wh. Proto používám lambda filtr k převodu hodnoty z Wh na kWh vydělením 1000.
```yaml ```yaml
sensor: sensor:
- platform: xt211 - platform: dlms_push
id: active_energy_consumed id: active_energy_consumed
name: "Energy" name: "Energy"
obis_code: 1.0.1.8.0.255 obis_code: 1.0.1.8.0.255
@ -204,7 +227,7 @@ Binární senzor má hodnotu `false`, pokud je hodnota 0, a hodnotu `true`, poku
```yaml ```yaml
binary_sensor: binary_sensor:
- platform: xt211 - platform: dlms_push
name: "Relay 1" name: "Relay 1"
obis_code: 0.1.96.3.10.255 obis_code: 0.1.96.3.10.255
``` ```
@ -213,7 +236,7 @@ binary_sensor:
```yaml ```yaml
text_sensor: text_sensor:
- platform: xt211 - platform: dlms_push
name: "Serial number" name: "Serial number"
obis_code: 0.0.96.1.1.255 obis_code: 0.0.96.1.1.255
entity_category: diagnostic entity_category: diagnostic
@ -301,7 +324,7 @@ sensor:
[... pulzní měřič z výše uvedeného configu ...] [... pulzní měřič z výše uvedeného configu ...]
- platform: xt211 - platform: dlms_push
id: active_power id: active_power
name: "Active power consumption" name: "Active power consumption"
obis_code: 1.0.1.7.0.255 obis_code: 1.0.1.7.0.255

View File

@ -140,11 +140,11 @@ Power Led (optional):
# Custom ESPHome component # Custom ESPHome component
To read out the DLMS/Cosem PUSH messages from the Sagecom XT211 meter, I modified the existing [esphome-dlms-cosem](https://github.com/latonita/esphome-dlms-cosem). There are two versions in this repository:
- `xt211` - the original version that I used for my setup, which is based on the [esphome-dlms-cosem](https://github.com/latonita/esphome-dlms-cosem)
- `dlms_push` - written from scratch (no longer using Gurux Libraries) using common esphome component structure, it is more flexible and easier to maintain.
Removed the polling functionality, fixed some bugs, and added support for binary sensors. I will be using and maintaining the `dlms_push` version, but the `xt211` version is still available for reference and for those who want to use it as is.
For a more detailed description of the component, see the [esphome-dlms-cosem repository](https://github.com/latonita/esphome-dlms-cosem) from [latonita](https://github.com/latonita)
## ESPHome configuration ## ESPHome configuration
@ -164,34 +164,48 @@ Add the external component to your ESPHome configuration:
```yaml ```yaml
external_components: external_components:
- source: github://Tomer27cz/xt211 - source: github://Tomer27cz/xt211
components: [xt211] components: [dlms_push]
refresh: 1s refresh: 1s
``` ```
Then configure the DLMS/Cosem component: The configuration options are:
- push_show_log: true (optional, for debugging purposes - shows all received PUSH messages in the log) - `show_log` (optional, default: `false`) - whether to show the log of the DLMS/COSEM communication. This is useful for debugging and first setup, but it can be quite verbose.
- `receive_timeout` (optional, default: `50ms`) - the timeout for receiving data from the meter. If the meter does not send any data within this time, the communication is considered finished and it will be processed.
- `custom_pattern` (optional) - custom COSEM object pattern
Disable the log onece everything is working fine. Disable the log onece everything is working fine.
```yaml ```yaml
logger:
baud_rate: 0
uart: uart:
id: bus_1 id: bus_1
rx_pin:
number: GPIO21
mode:
input: true
pullup: true
tx_pin: GPIO20 tx_pin: GPIO20
rx_pin: GPIO21
baud_rate: 9600 baud_rate: 9600
data_bits: 8 data_bits: 8
parity: NONE parity: NONE
stop_bits: 1 stop_bits: 1
xt211: dlms_push:
push_show_log: true id: my_dlms_meter
uart_id: bus_1
``` ```
In newer versions of ESPHome (esp-idf > 5.x) , the `uart` pins need to be pulled up, otherwise the communication will not work. This is because the default state of the pins was changed to floating.
I am using the same pins for the RS485 converter as for the logger, so I had to disable the logger by setting the `baud_rate` to 0. You can use different pins for the logger if you want to keep it enabled.
### Number sensor (`sensor`) ### Number sensor (`sensor`)
My electricity consumption is measured in kWh, but the meter sends the value in Wh. Therefore, I use a lambda filter to convert the value from Wh to kWh by dividing it by 1000. My electricity consumption is measured in kWh, but the meter sends the value in Wh. Therefore, I use a lambda filter to convert the value from Wh to kWh by dividing it by 1000.
```yaml ```yaml
sensor: sensor:
- platform: xt211 - platform: dlms_push
id: active_energy_consumed id: active_energy_consumed
name: "Energy" name: "Energy"
obis_code: 1.0.1.8.0.255 obis_code: 1.0.1.8.0.255
@ -208,7 +222,7 @@ The binary sensor is `false` when the value is 0, and `true` when the value is a
```yaml ```yaml
binary_sensor: binary_sensor:
- platform: xt211 - platform: dlms_push
name: "Relay 1" name: "Relay 1"
obis_code: 0.1.96.3.10.255 obis_code: 0.1.96.3.10.255
``` ```
@ -218,7 +232,7 @@ The text sensor is used to display string values sent by the meter.
```yaml ```yaml
text_sensor: text_sensor:
- platform: xt211 - platform: dlms_push
name: "Serial number" name: "Serial number"
obis_code: 0.0.96.1.1.255 obis_code: 0.0.96.1.1.255
entity_category: diagnostic entity_category: diagnostic
@ -306,7 +320,7 @@ sensor:
[... pulse meter from above ...] [... pulse meter from above ...]
- platform: xt211 - platform: dlms_push
id: active_power id: active_power
name: "Active power consumption" name: "Active power consumption"
obis_code: 1.0.1.7.0.255 obis_code: 1.0.1.7.0.255

View File

@ -0,0 +1,46 @@
import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID
DEPENDENCIES = ["uart"]
CONF_RECEIVE_TIMEOUT = "receive_timeout"
CONF_SHOW_LOG = "show_log"
CONF_CUSTOM_PATTERN = "custom_pattern"
# Define the namespace and the Hub component class
dlms_push_ns = cg.esphome_ns.namespace("dlms_push")
DlmsPushComponent = dlms_push_ns.class_("DlmsPushComponent", cg.Component, uart.UARTDevice)
def obis_code(value):
value = cv.string(value)
# Validate standard OBIS format: A.B.C.D.E.F (e.g., 1.0.1.8.0.255)
match = re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", value)
if match is None:
raise cv.Invalid(f"{value} is not a valid OBIS code (expected format: A.B.C.D.E.F)")
return value
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DlmsPushComponent),
cv.Optional(CONF_RECEIVE_TIMEOUT, default="50ms"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_SHOW_LOG, default=False): cv.boolean,
cv.Optional(CONF_CUSTOM_PATTERN, default=""): cv.string,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
# Apply configuration to the C++ component
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT]))
cg.add(var.set_show_log(config[CONF_SHOW_LOG]))
cg.add(var.set_custom_pattern(config[CONF_CUSTOM_PATTERN]))

View File

@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from . import DlmsPushComponent, obis_code
DEPENDENCIES = ["dlms_push"]
CONF_DLMS_PUSH_ID = "dlms_push_id"
CONF_OBIS_CODE = "obis_code"
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
{
cv.GenerateID(CONF_DLMS_PUSH_ID): cv.use_id(DlmsPushComponent),
cv.Required(CONF_OBIS_CODE): obis_code,
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_PUSH_ID])
var = await binary_sensor.new_binary_sensor(config)
cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var))

View File

@ -0,0 +1,683 @@
#include "dlms_parser.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <cmath>
#include <cstring>
#include <sstream>
#include <iomanip>
#include <algorithm>
namespace esphome {
namespace dlms_push {
static const char *const TAG = "dlms_parser";
DlmsParser::DlmsParser() {
this->load_default_patterns_();
}
void DlmsParser::load_default_patterns_() {
this->register_pattern_dsl_("T1", "TC,TO,TS,TV", 10);
this->register_pattern_dsl_("T2", "TO,TV,TSU", 10);
this->register_pattern_dsl_("T3", "TV,TC,TSU,TO", 10);
this->register_pattern_dsl_("U.ZPA", "F,C,O,A,TV", 10);
}
void DlmsParser::register_custom_pattern(const std::string &dsl) {
this->register_pattern_dsl_("CUSTOM", dsl, 0); // Priority 0 to try this first
}
size_t DlmsParser::parse(const uint8_t *buffer, size_t length, DlmsDataCallback callback, bool show_log) {
if (buffer == nullptr || length == 0) {
if (show_log) ESP_LOGV(TAG, "Buffer is null or empty");
return 0;
}
this->buffer_ = buffer;
this->buffer_len_ = length;
this->pos_ = 0;
this->callback_ = callback;
this->show_log_ = show_log;
this->objects_found_ = 0;
if (this->show_log_) ESP_LOGD(TAG, "Starting to parse buffer of length %zu", length);
// Skip to notification flag 0x0F
while (this->pos_ < this->buffer_len_) {
if (this->read_byte_() == 0x0F) {
if (this->show_log_) ESP_LOGD(TAG, "Found notification flag 0x0F at position %zu", this->pos_ - 1);
break;
}
}
// Strictly skip Invoke-ID/Priority (5 bytes)
for (int i = 0; i < 5 && this->pos_ < this->buffer_len_; i++) {
this->pos_++;
}
// Check for datetime object before the data and skip it if present
if (this->test_if_date_time_12b_()) {
if (this->show_log_) ESP_LOGV(TAG, "Skipping datetime object at position %zu", this->pos_);
this->pos_ += 12;
}
// First byte after flag should be the data type (usually Structure or Array)
uint8_t start_type = this->read_byte_();
if (start_type != DLMS_DATA_TYPE_STRUCTURE && start_type != DLMS_DATA_TYPE_ARRAY) {
if (this->show_log_) ESP_LOGW(TAG, "Expected STRUCTURE or ARRAY after header, found type %02X at position %zu", start_type, this->pos_ - 1);
return 0;
}
// Trigger recursive parsing
bool success = this->parse_element_(start_type, 0);
if (!success && this->show_log_) {
ESP_LOGV(TAG, "Some errors occurred parsing DLMS data, or unexpected end of buffer.");
}
if (this->show_log_) ESP_LOGD(TAG, "Parsing completed. Processed %zu bytes, found %zu objects", this->pos_, this->objects_found_);
return this->objects_found_;
}
uint8_t DlmsParser::read_byte_() {
if (this->pos_ >= this->buffer_len_) return 0xFF;
return this->buffer_[this->pos_++];
}
uint16_t DlmsParser::read_u16_() {
if (this->pos_ + 1 >= this->buffer_len_) return 0xFFFF;
uint16_t val = (this->buffer_[this->pos_] << 8) | this->buffer_[this->pos_ + 1];
this->pos_ += 2;
return val;
}
uint32_t DlmsParser::read_u32_() {
if (this->pos_ + 3 >= this->buffer_len_) return 0xFFFFFFFF;
uint32_t val = (this->buffer_[this->pos_] << 24) | (this->buffer_[this->pos_ + 1] << 16) |
(this->buffer_[this->pos_ + 2] << 8) | this->buffer_[this->pos_ + 3];
this->pos_ += 4;
return val;
}
bool DlmsParser::test_if_date_time_12b_() {
if (this->pos_ + 12 > this->buffer_len_) return false;
const uint8_t *buf = &this->buffer_[this->pos_];
uint16_t year = (buf[0] << 8) | buf[1];
if (!(year == 0x0000 || (year >= 1970 && year <= 2100))) return false;
if (!(buf[2] == 0xFF || (buf[2] >= 1 && buf[2] <= 12))) return false;
if (!(buf[3] == 0xFF || (buf[3] >= 1 && buf[3] <= 31))) return false;
if (!(buf[4] == 0xFF || (buf[4] >= 1 && buf[4] <= 7))) return false;
if (!(buf[5] == 0xFF || buf[5] <= 23)) return false;
if (!(buf[6] == 0xFF || buf[6] <= 59)) return false;
if (!(buf[7] == 0xFF || buf[7] <= 59)) return false;
// Hundredths of second
uint8_t ms = buf[8];
if (!(ms == 0xFF || ms <= 99)) return false;
// Deviation (timezone offset, signed, 2 bytes)
uint16_t u_dev = (buf[9] << 8) | buf[10];
int16_t s_dev = (int16_t) u_dev;
if (!((s_dev == (int16_t) 0x8000 || (s_dev >= -720 && s_dev <= 720)))) return false;
return true;
}
int DlmsParser::get_data_type_size_(DlmsDataType type) {
switch (type) {
case DLMS_DATA_TYPE_NONE: return 0;
case DLMS_DATA_TYPE_BOOLEAN:
case DLMS_DATA_TYPE_INT8:
case DLMS_DATA_TYPE_UINT8:
case DLMS_DATA_TYPE_ENUM: return 1;
case DLMS_DATA_TYPE_INT16:
case DLMS_DATA_TYPE_UINT16: return 2;
case DLMS_DATA_TYPE_INT32:
case DLMS_DATA_TYPE_UINT32:
case DLMS_DATA_TYPE_FLOAT32: return 4;
case DLMS_DATA_TYPE_INT64:
case DLMS_DATA_TYPE_UINT64:
case DLMS_DATA_TYPE_FLOAT64: return 8;
case DLMS_DATA_TYPE_DATETIME: return 12;
case DLMS_DATA_TYPE_DATE: return 5;
case DLMS_DATA_TYPE_TIME: return 4;
default: return -1; // Variable or complex
}
}
bool DlmsParser::is_value_data_type_(DlmsDataType type) {
switch (type) {
case DLMS_DATA_TYPE_ARRAY:
case DLMS_DATA_TYPE_STRUCTURE:
case DLMS_DATA_TYPE_COMPACT_ARRAY:
return false;
case DLMS_DATA_TYPE_NONE:
case DLMS_DATA_TYPE_BOOLEAN:
case DLMS_DATA_TYPE_BIT_STRING:
case DLMS_DATA_TYPE_INT32:
case DLMS_DATA_TYPE_UINT32:
case DLMS_DATA_TYPE_OCTET_STRING:
case DLMS_DATA_TYPE_STRING:
case DLMS_DATA_TYPE_BINARY_CODED_DESIMAL:
case DLMS_DATA_TYPE_STRING_UTF8:
case DLMS_DATA_TYPE_INT8:
case DLMS_DATA_TYPE_INT16:
case DLMS_DATA_TYPE_UINT8:
case DLMS_DATA_TYPE_UINT16:
case DLMS_DATA_TYPE_INT64:
case DLMS_DATA_TYPE_UINT64:
case DLMS_DATA_TYPE_ENUM:
case DLMS_DATA_TYPE_FLOAT32:
case DLMS_DATA_TYPE_FLOAT64:
case DLMS_DATA_TYPE_DATETIME:
case DLMS_DATA_TYPE_DATE:
case DLMS_DATA_TYPE_TIME:
return true;
default:
return false;
}
}
bool DlmsParser::skip_data_(uint8_t type) {
int data_size = this->get_data_type_size_((DlmsDataType)type);
if (data_size == 0) return true;
if (data_size > 0) {
if (this->pos_ + data_size > this->buffer_len_) return false;
this->pos_ += data_size;
} else {
uint8_t first_byte = this->read_byte_();
if (first_byte == 0xFF) return false;
uint32_t length = first_byte;
// Handle DLMS multi-byte length fields
if (first_byte > 127) {
uint8_t num_bytes = first_byte & 0x7F;
length = 0;
for (int i = 0; i < num_bytes; i++) {
uint8_t b = this->read_byte_();
if (b == 0xFF && this->pos_ >= this->buffer_len_) return false;
length = (length << 8) | b;
}
}
uint32_t skip_bytes = length;
// DLMS Bit strings designate their length in bits, so we must adjust the byte skip
if (type == DLMS_DATA_TYPE_BIT_STRING) {
skip_bytes = (length + 7) / 8;
}
if (this->pos_ + skip_bytes > this->buffer_len_) return false;
if (this->show_log_) {
ESP_LOGVV(TAG, "Skipping variable data of type %s (bytes: %u) at position %zu",
this->dlms_data_type_to_string_((DlmsDataType)type), skip_bytes, this->pos_);
}
this->pos_ += skip_bytes;
}
return true;
}
bool DlmsParser::parse_element_(uint8_t type, uint8_t depth) {
if (type == DLMS_DATA_TYPE_STRUCTURE || type == DLMS_DATA_TYPE_ARRAY) {
return this->parse_sequence_(type, depth);
}
return this->skip_data_(type);
}
bool DlmsParser::parse_sequence_(uint8_t type, uint8_t depth) {
uint8_t elements_count = this->read_byte_();
if (elements_count == 0xFF) {
if (this->show_log_) ESP_LOGVV(TAG, "Invalid sequence length at position %zu", this->pos_ - 1);
return false;
}
if (this->show_log_) {
ESP_LOGD(TAG, "Parsing %s with %d elements at position %zu (depth %d)",
type == DLMS_DATA_TYPE_STRUCTURE ? "STRUCTURE" : "ARRAY", elements_count, this->pos_ - 1, depth);
}
uint8_t elements_consumed = 0;
while (elements_consumed < elements_count) {
size_t original_position = this->pos_;
if (this->try_match_patterns_(elements_consumed)) {
elements_consumed += this->last_pattern_elements_consumed_ ? this->last_pattern_elements_consumed_ : 1;
this->last_pattern_elements_consumed_ = 0;
continue;
}
if (this->pos_ >= this->buffer_len_) {
if (this->show_log_) ESP_LOGV(TAG, "Unexpected end while reading element %d of %s", elements_consumed + 1, type == DLMS_DATA_TYPE_STRUCTURE ? "STRUCTURE" : "ARRAY");
return false;
}
uint8_t elem_type = this->read_byte_();
if (!this->parse_element_(elem_type, depth + 1)) return false;
elements_consumed++;
if (this->pos_ == original_position) {
if (this->show_log_) ESP_LOGV(TAG, "No progress parsing element %d at position %zu, aborting to avoid infinite loop", elements_consumed, original_position);
return false;
}
}
return true;
}
bool DlmsParser::capture_generic_value_(AxdrCaptures &c) {
uint8_t vt = this->read_byte_();
if (!this->is_value_data_type_((DlmsDataType)vt)) return false;
int ds = this->get_data_type_size_((DlmsDataType)vt);
if (ds > 0) {
if (this->pos_ + ds > this->buffer_len_) return false;
c.value_ptr = &this->buffer_[this->pos_];
c.value_len = ds;
this->pos_ += ds;
} else if (ds == 0) {
c.value_ptr = nullptr;
c.value_len = 0;
} else {
uint8_t first_byte = this->read_byte_();
if (first_byte == 0xFF) return false;
uint32_t length = first_byte;
if (first_byte > 127) {
uint8_t num_bytes = first_byte & 0x7F;
length = 0;
for (int i = 0; i < num_bytes; i++) {
uint8_t b = this->read_byte_();
if (b == 0xFF && this->pos_ >= this->buffer_len_) return false;
length = (length << 8) | b;
}
}
uint32_t data_bytes = length;
if (vt == DLMS_DATA_TYPE_BIT_STRING) {
data_bytes = (length + 7) / 8;
}
if (this->pos_ + data_bytes > this->buffer_len_) return false;
c.value_ptr = &this->buffer_[this->pos_];
c.value_len = data_bytes > 255 ? 255 : data_bytes;
this->pos_ += data_bytes;
}
c.value_type = (DlmsDataType)vt;
return true;
}
bool DlmsParser::try_match_patterns_(uint8_t elem_idx) {
for (const auto &p : this->patterns_) {
uint8_t consumed = 0;
size_t saved_position = this->pos_;
if (this->match_pattern_(elem_idx, p, consumed)) {
this->last_pattern_elements_consumed_ = consumed;
return true;
}
this->pos_ = saved_position; // Backtrack if match failed
}
return false;
}
bool DlmsParser::match_pattern_(uint8_t elem_idx, const AxdrDescriptorPattern &pat, uint8_t &elements_consumed_at_level0) {
AxdrCaptures cap{};
elements_consumed_at_level0 = 0;
uint8_t level = 0;
auto consume_one = [&]() { if (level == 0) elements_consumed_at_level0++; };
size_t initial_position = this->pos_;
for (const auto &step : pat.steps) {
switch (step.type) {
case AxdrTokenType::EXPECT_TO_BE_FIRST:
if (elem_idx != 0) return false;
break;
case AxdrTokenType::EXPECT_TYPE_EXACT:
if (this->read_byte_() != step.param_u8_a) return false;
consume_one();
break;
case AxdrTokenType::EXPECT_TYPE_U_I_8: {
uint8_t t = this->read_byte_();
if (t != DLMS_DATA_TYPE_INT8 && t != DLMS_DATA_TYPE_UINT8) return false;
consume_one();
break;
}
case AxdrTokenType::EXPECT_CLASS_ID_UNTAGGED: {
uint16_t v = this->read_u16_();
if (v > 0x00FF) return false; // Match max typical class ID
cap.class_id = v;
break;
}
case AxdrTokenType::EXPECT_OBIS6_TAGGED:
if (this->read_byte_() != DLMS_DATA_TYPE_OCTET_STRING) return false;
if (this->read_byte_() != 6) return false;
if (this->pos_ + 6 > this->buffer_len_) return false;
cap.obis = &this->buffer_[this->pos_];
this->pos_ += 6;
consume_one();
break;
case AxdrTokenType::EXPECT_OBIS6_UNTAGGED:
if (this->pos_ + 6 > this->buffer_len_) return false;
cap.obis = &this->buffer_[this->pos_];
this->pos_ += 6;
break;
case AxdrTokenType::EXPECT_ATTR8_UNTAGGED:
if (this->read_byte_() == 0) return false;
break;
case AxdrTokenType::EXPECT_VALUE_GENERIC:
if (!this->capture_generic_value_(cap)) return false;
consume_one();
break;
case AxdrTokenType::EXPECT_STRUCTURE_N:
if (this->read_byte_() != DLMS_DATA_TYPE_STRUCTURE) return false;
if (this->read_byte_() != step.param_u8_a) return false;
consume_one();
break;
case AxdrTokenType::EXPECT_SCALER_TAGGED:
if (this->read_byte_() != DLMS_DATA_TYPE_INT8) return false;
cap.scaler = (int8_t)this->read_byte_();
cap.has_scaler_unit = true;
consume_one();
break;
case AxdrTokenType::EXPECT_UNIT_ENUM_TAGGED:
if (this->read_byte_() != DLMS_DATA_TYPE_ENUM) return false;
cap.unit_enum = this->read_byte_();
cap.has_scaler_unit = true;
consume_one();
break;
case AxdrTokenType::GOING_DOWN: level++; break;
case AxdrTokenType::GOING_UP: level--; break;
}
}
if (elements_consumed_at_level0 == 0) elements_consumed_at_level0 = 1;
cap.elem_idx = initial_position;
this->emit_object_(pat, cap);
return true;
}
void DlmsParser::emit_object_(const AxdrDescriptorPattern &pat, const AxdrCaptures &c) {
if (!c.obis || !this->callback_) return;
std::string obis_str = this->obis_to_string_(c.obis);
float raw_val_f = this->data_as_float_(c.value_type, c.value_ptr, c.value_len);
float val_f = raw_val_f;
std::string val_s = this->data_as_string_(c.value_type, c.value_ptr, c.value_len);
bool is_numeric = (c.value_type != DLMS_DATA_TYPE_OCTET_STRING &&
c.value_type != DLMS_DATA_TYPE_STRING &&
c.value_type != DLMS_DATA_TYPE_STRING_UTF8);
if (c.has_scaler_unit && is_numeric) {
val_f *= std::pow(10, c.scaler);
}
if (this->show_log_) {
ESP_LOGD(TAG, "Pattern match '%s' at idx %u ===============", pat.name.c_str(), c.elem_idx);
uint16_t cid = c.class_id ? c.class_id : pat.default_class_id;
ESP_LOGI(TAG, "Found attribute descriptor: class_id=%d, obis=%s", cid, obis_str.c_str());
if (c.has_scaler_unit) {
ESP_LOGI(TAG, "Value type: %s, len %d, scaler %d, unit %d",
this->dlms_data_type_to_string_(c.value_type), c.value_len, c.scaler, c.unit_enum);
} else {
ESP_LOGI(TAG, "Value type: %s, len %d", this->dlms_data_type_to_string_(c.value_type), c.value_len);
}
if (c.value_ptr && c.value_len > 0) {
ESP_LOGI(TAG, " as hex dump : %s", esphome::format_hex_pretty(c.value_ptr, c.value_len).c_str());
}
ESP_LOGI(TAG, " as string :'%s'", val_s.c_str());
ESP_LOGI(TAG, " as number : %f", raw_val_f);
if (c.has_scaler_unit && is_numeric) {
ESP_LOGI(TAG, " as number * scaler : %f", val_f);
}
}
this->callback_(obis_str, val_f, val_s, is_numeric);
this->objects_found_++;
}
float DlmsParser::data_as_float_(DlmsDataType value_type, const uint8_t *ptr, uint8_t len) {
if (!ptr || len == 0) return 0.0f;
auto be16 = [](const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); };
auto be32 = [](const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; };
auto be64 = [](const uint8_t *p) {
return ((uint64_t)p[0] << 56) | ((uint64_t)p[1] << 48) | ((uint64_t)p[2] << 40) | ((uint64_t)p[3] << 32) |
((uint64_t)p[4] << 24) | ((uint64_t)p[5] << 16) | ((uint64_t)p[6] << 8) | (uint64_t)p[7];
};
switch (value_type) {
case DLMS_DATA_TYPE_BOOLEAN:
case DLMS_DATA_TYPE_ENUM:
case DLMS_DATA_TYPE_UINT8: return static_cast<float>(ptr[0]);
case DLMS_DATA_TYPE_INT8: return static_cast<float>(static_cast<int8_t>(ptr[0]));
case DLMS_DATA_TYPE_BIT_STRING: return (len > 0 && ptr) ? static_cast<float>(ptr[0]) : 0.0f;
case DLMS_DATA_TYPE_UINT16: return len >= 2 ? static_cast<float>(be16(ptr)) : 0.0f;
case DLMS_DATA_TYPE_INT16: return len >= 2 ? static_cast<float>(static_cast<int16_t>(be16(ptr))) : 0.0f;
case DLMS_DATA_TYPE_UINT32: return len >= 4 ? static_cast<float>(be32(ptr)) : 0.0f;
case DLMS_DATA_TYPE_INT32: return len >= 4 ? static_cast<float>(static_cast<int32_t>(be32(ptr))) : 0.0f;
case DLMS_DATA_TYPE_UINT64: return len >= 8 ? static_cast<float>(be64(ptr)) : 0.0f;
case DLMS_DATA_TYPE_INT64: return len >= 8 ? static_cast<float>(static_cast<int64_t>(be64(ptr))) : 0.0f;
case DLMS_DATA_TYPE_FLOAT32: {
if (len < 4) return 0.0f;
uint32_t i32 = be32(ptr);
float f;
std::memcpy(&f, &i32, sizeof(float));
return f;
}
case DLMS_DATA_TYPE_FLOAT64: {
if (len < 8) return 0.0f;
uint64_t i64 = be64(ptr);
double d;
std::memcpy(&d, &i64, sizeof(double));
return static_cast<float>(d);
}
default: return 0.0f;
}
}
std::string DlmsParser::data_as_string_(DlmsDataType value_type, const uint8_t *ptr, uint8_t len) {
if (!ptr || len == 0) return "";
auto hex_of = [](const uint8_t *p, uint8_t l) {
std::ostringstream ss;
ss << std::hex << std::setfill('0');
for (uint8_t i = 0; i < l; i++) {
ss << std::setw(2) << static_cast<int>(p[i]);
}
return ss.str();
};
auto be16 = [](const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); };
auto be32 = [](const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; };
auto be64 = [](const uint8_t *p) {
uint64_t v = 0;
for (int i = 0; i < 8; i++) v = (v << 8) | p[i];
return v;
};
switch (value_type) {
case DLMS_DATA_TYPE_OCTET_STRING:
case DLMS_DATA_TYPE_STRING:
case DLMS_DATA_TYPE_STRING_UTF8:
return std::string(reinterpret_cast<const char *>(ptr), len);
case DLMS_DATA_TYPE_BIT_STRING:
case DLMS_DATA_TYPE_BINARY_CODED_DESIMAL:
case DLMS_DATA_TYPE_DATETIME:
case DLMS_DATA_TYPE_DATE:
case DLMS_DATA_TYPE_TIME:
return hex_of(ptr, len);
case DLMS_DATA_TYPE_BOOLEAN:
case DLMS_DATA_TYPE_ENUM:
case DLMS_DATA_TYPE_UINT8:
return std::to_string(static_cast<unsigned>(ptr[0]));
case DLMS_DATA_TYPE_INT8:
return std::to_string(static_cast<int>(static_cast<int8_t>(ptr[0])));
case DLMS_DATA_TYPE_UINT16:
return len >= 2 ? std::to_string(be16(ptr)) : "";
case DLMS_DATA_TYPE_INT16:
return len >= 2 ? std::to_string(static_cast<int16_t>(be16(ptr))) : "";
case DLMS_DATA_TYPE_UINT32:
return len >= 4 ? std::to_string(be32(ptr)) : "";
case DLMS_DATA_TYPE_INT32:
return len >= 4 ? std::to_string(static_cast<int32_t>(be32(ptr))) : "";
case DLMS_DATA_TYPE_UINT64:
return len >= 8 ? std::to_string(be64(ptr)) : "";
case DLMS_DATA_TYPE_INT64:
return len >= 8 ? std::to_string(static_cast<int64_t>(be64(ptr))) : "";
case DLMS_DATA_TYPE_FLOAT32:
case DLMS_DATA_TYPE_FLOAT64: {
std::ostringstream ss;
ss << this->data_as_float_(value_type, ptr, len);
return ss.str();
}
default:
return "";
}
}
std::string DlmsParser::obis_to_string_(const uint8_t *obis) {
char buf[32];
snprintf(buf, sizeof(buf), "%u.%u.%u.%u.%u.%u", obis[0], obis[1], obis[2], obis[3], obis[4], obis[5]);
return std::string(buf);
}
const char *DlmsParser::dlms_data_type_to_string_(DlmsDataType vt) {
switch (vt) {
case DLMS_DATA_TYPE_NONE: return "NONE";
case DLMS_DATA_TYPE_ARRAY: return "ARRAY";
case DLMS_DATA_TYPE_STRUCTURE: return "STRUCTURE";
case DLMS_DATA_TYPE_BOOLEAN: return "BOOLEAN";
case DLMS_DATA_TYPE_BIT_STRING: return "BIT_STRING";
case DLMS_DATA_TYPE_INT32: return "INT32";
case DLMS_DATA_TYPE_UINT32: return "UINT32";
case DLMS_DATA_TYPE_OCTET_STRING: return "OCTET_STRING";
case DLMS_DATA_TYPE_STRING: return "STRING";
case DLMS_DATA_TYPE_STRING_UTF8: return "STRING_UTF8";
case DLMS_DATA_TYPE_BINARY_CODED_DESIMAL: return "BINARY_CODED_DESIMAL";
case DLMS_DATA_TYPE_INT8: return "INT8";
case DLMS_DATA_TYPE_INT16: return "INT16";
case DLMS_DATA_TYPE_UINT8: return "UINT8";
case DLMS_DATA_TYPE_UINT16: return "UINT16";
case DLMS_DATA_TYPE_COMPACT_ARRAY: return "COMPACT_ARRAY";
case DLMS_DATA_TYPE_INT64: return "INT64";
case DLMS_DATA_TYPE_UINT64: return "UINT64";
case DLMS_DATA_TYPE_ENUM: return "ENUM";
case DLMS_DATA_TYPE_FLOAT32: return "FLOAT32";
case DLMS_DATA_TYPE_FLOAT64: return "FLOAT64";
case DLMS_DATA_TYPE_DATETIME: return "DATETIME";
case DLMS_DATA_TYPE_DATE: return "DATE";
case DLMS_DATA_TYPE_TIME: return "TIME";
default: return "UNKNOWN";
}
}
void DlmsParser::register_pattern_dsl_(const std::string &name, const std::string &dsl, int priority) {
AxdrDescriptorPattern pat{name, priority, {}, 0};
auto trim = [](const std::string &s) {
size_t b = s.find_first_not_of(" \t\r\n");
size_t e = s.find_last_not_of(" \t\r\n");
if (b == std::string::npos) return std::string();
return s.substr(b, e - b + 1);
};
std::list<std::string> tokens;
std::string current;
int paren = 0;
for (char c : dsl) {
if (c == '(') {
paren++;
current.push_back(c);
} else if (c == ')') {
paren--;
current.push_back(c);
} else if (c == ',' && paren == 0) {
tokens.push_back(trim(current));
current.clear();
} else {
current.push_back(c);
}
}
if (!current.empty()) tokens.push_back(trim(current));
for (auto it = tokens.begin(); it != tokens.end(); ++it) {
std::string tok = *it;
if (tok.empty()) continue;
if (tok == "F") pat.steps.push_back({AxdrTokenType::EXPECT_TO_BE_FIRST});
else if (tok == "C") pat.steps.push_back({AxdrTokenType::EXPECT_CLASS_ID_UNTAGGED});
else if (tok == "TC") {
pat.steps.push_back({AxdrTokenType::EXPECT_TYPE_EXACT, DLMS_DATA_TYPE_UINT16});
pat.steps.push_back({AxdrTokenType::EXPECT_CLASS_ID_UNTAGGED});
}
else if (tok == "O") pat.steps.push_back({AxdrTokenType::EXPECT_OBIS6_UNTAGGED});
else if (tok == "TO") pat.steps.push_back({AxdrTokenType::EXPECT_OBIS6_TAGGED});
else if (tok == "A") pat.steps.push_back({AxdrTokenType::EXPECT_ATTR8_UNTAGGED});
else if (tok == "TA") {
pat.steps.push_back({AxdrTokenType::EXPECT_TYPE_U_I_8});
pat.steps.push_back({AxdrTokenType::EXPECT_ATTR8_UNTAGGED});
}
else if (tok == "TS") pat.steps.push_back({AxdrTokenType::EXPECT_SCALER_TAGGED});
else if (tok == "TU") pat.steps.push_back({AxdrTokenType::EXPECT_UNIT_ENUM_TAGGED});
else if (tok == "TSU") {
pat.steps.push_back({AxdrTokenType::EXPECT_STRUCTURE_N, 2});
pat.steps.push_back({AxdrTokenType::GOING_DOWN});
pat.steps.push_back({AxdrTokenType::EXPECT_SCALER_TAGGED});
pat.steps.push_back({AxdrTokenType::EXPECT_UNIT_ENUM_TAGGED});
pat.steps.push_back({AxdrTokenType::GOING_UP});
}
else if (tok == "V" || tok == "TV") pat.steps.push_back({AxdrTokenType::EXPECT_VALUE_GENERIC});
else if (tok.size() >= 2 && tok.substr(0, 2) == "S(") {
size_t l = tok.find('(');
size_t r = tok.rfind(')');
if (l != std::string::npos && r != std::string::npos && r > l + 1) {
std::string inner = tok.substr(l + 1, r - l - 1);
std::list<std::string> inner_tokens;
std::string cur;
for (char c2 : inner) {
if (c2 == ',') {
inner_tokens.push_back(trim(cur));
cur.clear();
} else {
cur.push_back(c2);
}
}
if (!cur.empty()) inner_tokens.push_back(trim(cur));
if (!inner_tokens.empty()) {
pat.steps.push_back({AxdrTokenType::EXPECT_STRUCTURE_N, static_cast<uint8_t>(inner_tokens.size())});
inner_tokens.push_front("DN");
inner_tokens.push_back("UP");
tokens.insert(std::next(it), inner_tokens.begin(), inner_tokens.end());
}
}
}
else if (tok == "DN") pat.steps.push_back({AxdrTokenType::GOING_DOWN});
else if (tok == "UP") pat.steps.push_back({AxdrTokenType::GOING_UP});
}
// Insert maintaining priority sort order
auto it = std::upper_bound(this->patterns_.begin(), this->patterns_.end(), pat,
[](const AxdrDescriptorPattern &a, const AxdrDescriptorPattern &b) { return a.priority < b.priority; });
this->patterns_.insert(it, pat);
}
} // namespace dlms_push
} // namespace esphome

View File

@ -0,0 +1,133 @@
#pragma once
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
#include <list>
namespace esphome {
namespace dlms_push {
enum DlmsDataType : uint8_t {
DLMS_DATA_TYPE_NONE = 0,
DLMS_DATA_TYPE_ARRAY = 1,
DLMS_DATA_TYPE_STRUCTURE = 2,
DLMS_DATA_TYPE_BOOLEAN = 3,
DLMS_DATA_TYPE_BIT_STRING = 4,
DLMS_DATA_TYPE_INT32 = 5,
DLMS_DATA_TYPE_UINT32 = 6,
DLMS_DATA_TYPE_OCTET_STRING = 9,
DLMS_DATA_TYPE_STRING = 10,
DLMS_DATA_TYPE_STRING_UTF8 = 12,
DLMS_DATA_TYPE_BINARY_CODED_DESIMAL = 13,
DLMS_DATA_TYPE_INT8 = 15,
DLMS_DATA_TYPE_INT16 = 16,
DLMS_DATA_TYPE_UINT8 = 17,
DLMS_DATA_TYPE_UINT16 = 18,
DLMS_DATA_TYPE_COMPACT_ARRAY = 19,
DLMS_DATA_TYPE_INT64 = 20,
DLMS_DATA_TYPE_UINT64 = 21,
DLMS_DATA_TYPE_ENUM = 22,
DLMS_DATA_TYPE_FLOAT32 = 23,
DLMS_DATA_TYPE_FLOAT64 = 24,
DLMS_DATA_TYPE_DATETIME = 25,
DLMS_DATA_TYPE_DATE = 26,
DLMS_DATA_TYPE_TIME = 27
};
// Callback for the hub: OBIS code (e.g. "1.0.1.8.0.255"), numeric value, string value, is_numeric flag
using DlmsDataCallback = std::function<void(const std::string &obis_code, float float_val, const std::string &str_val, bool is_numeric)>;
// --- Pattern Matching Enums & Structs ---
enum class AxdrTokenType : uint8_t {
EXPECT_TO_BE_FIRST,
EXPECT_TYPE_EXACT,
EXPECT_TYPE_U_I_8,
EXPECT_CLASS_ID_UNTAGGED,
EXPECT_OBIS6_TAGGED,
EXPECT_OBIS6_UNTAGGED,
EXPECT_ATTR8_UNTAGGED,
EXPECT_VALUE_GENERIC,
EXPECT_STRUCTURE_N,
EXPECT_SCALER_TAGGED,
EXPECT_UNIT_ENUM_TAGGED,
GOING_DOWN,
GOING_UP,
};
struct AxdrPatternStep {
AxdrTokenType type;
uint8_t param_u8_a{0};
};
struct AxdrDescriptorPattern {
std::string name;
int priority{0};
std::vector<AxdrPatternStep> steps;
uint16_t default_class_id{0};
};
struct AxdrCaptures {
uint32_t elem_idx{0};
uint16_t class_id{0};
const uint8_t *obis{nullptr};
DlmsDataType value_type{DlmsDataType::DLMS_DATA_TYPE_NONE};
const uint8_t *value_ptr{nullptr};
uint8_t value_len{0};
bool has_scaler_unit{false};
int8_t scaler{0};
uint8_t unit_enum{0};
};
class DlmsParser {
public:
DlmsParser();
// Registers a custom parsing pattern from the YAML config
void register_custom_pattern(const std::string &dsl);
// Parses the buffer and fires callbacks for each found sensor value
size_t parse(const uint8_t *buffer, size_t length, DlmsDataCallback callback, bool show_log);
private:
void register_pattern_dsl_(const std::string &name, const std::string &dsl, int priority);
void load_default_patterns_();
uint8_t read_byte_();
uint16_t read_u16_();
uint32_t read_u32_();
bool test_if_date_time_12b_();
int get_data_type_size_(DlmsDataType type);
bool is_value_data_type_(DlmsDataType type);
bool skip_data_(uint8_t type);
bool parse_element_(uint8_t type, uint8_t depth = 0);
bool parse_sequence_(uint8_t type, uint8_t depth = 0);
bool capture_generic_value_(AxdrCaptures &c);
bool try_match_patterns_(uint8_t elem_idx);
bool match_pattern_(uint8_t elem_idx, const AxdrDescriptorPattern &pat, uint8_t &elements_consumed_at_level0);
void emit_object_(const AxdrDescriptorPattern &pat, const AxdrCaptures &c);
float data_as_float_(DlmsDataType value_type, const uint8_t *ptr, uint8_t len);
std::string data_as_string_(DlmsDataType value_type, const uint8_t *ptr, uint8_t len);
std::string obis_to_string_(const uint8_t *obis);
const char *dlms_data_type_to_string_(DlmsDataType vt);
const uint8_t *buffer_{nullptr};
size_t buffer_len_{0};
size_t pos_{0};
DlmsDataCallback callback_;
bool show_log_{false};
size_t objects_found_{0};
uint8_t last_pattern_elements_consumed_{0};
std::vector<AxdrDescriptorPattern> patterns_;
};
} // namespace dlms_push
} // namespace esphome

View File

@ -0,0 +1,182 @@
#include "dlms_push.h"
#include "dlms_parser.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace dlms_push {
static const char *const TAG = "dlms_push";
DlmsPushComponent::DlmsPushComponent() {
this->parser_ = new DlmsParser();
}
void DlmsPushComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up DLMS PUSH Component...");
if (!this->custom_pattern_.empty()) {
this->parser_->register_custom_pattern(this->custom_pattern_);
}
this->rx_buffer_ = std::make_unique<uint8_t[]>(MAX_RX_BUFFER_SIZE);
this->rx_buffer_len_ = 0;
}
void DlmsPushComponent::dump_config() {
ESP_LOGCONFIG(TAG, "DLMS PUSH Component:");
ESP_LOGCONFIG(TAG, " Receive Timeout: %u ms", this->receive_timeout_ms_);
ESP_LOGCONFIG(TAG, " Show Log: %s", this->show_log_ ? "True" : "False");
if (!this->custom_pattern_.empty()) {
ESP_LOGCONFIG(TAG, " Custom Pattern: %s", this->custom_pattern_.c_str());
}
#ifdef USE_SENSOR
for (const auto &entry : this->sensors_) {
LOG_SENSOR(" ", "Numeric Sensor (OBIS)", entry.sensor);
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis.c_str());
}
#endif
#ifdef USE_TEXT_SENSOR
for (const auto &entry : this->text_sensors_) {
LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", entry.sensor);
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis.c_str());
}
#endif
#ifdef USE_BINARY_SENSOR
for (const auto &entry : this->binary_sensors_) {
LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", entry.sensor);
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis.c_str());
}
#endif
}
void DlmsPushComponent::loop() {
this->read_rx_buffer_();
if (this->receiving_ && (millis() - this->last_rx_char_time_ > this->receive_timeout_ms_)) {
this->receiving_ = false;
this->process_frame_();
}
}
void DlmsPushComponent::read_rx_buffer_() {
int available = this->available();
if (available == 0) return;
this->receiving_ = true;
this->last_rx_char_time_ = millis();
while (this->available()) {
if (this->rx_buffer_len_ >= MAX_RX_BUFFER_SIZE) {
ESP_LOGW(TAG, "RX Buffer overflow. Frame too large! Truncating.");
break;
}
uint8_t byte;
if (this->read_byte(&byte)) {
this->rx_buffer_[this->rx_buffer_len_++] = byte;
} else {
ESP_LOGW(TAG, "Failed to read byte from UART.");
break;
}
}
}
void DlmsPushComponent::process_frame_() {
if (this->rx_buffer_len_ == 0) return;
if (this->show_log_) {
ESP_LOGD(TAG, "Processing received push data");
ESP_LOGD(TAG, "Processing PUSH data frame with DLMS parser");
ESP_LOGD(TAG, "PUSH frame size: %zu bytes", this->rx_buffer_len_);
}
auto callback = [this](const std::string &obis_code, float float_val, const std::string &str_val, bool is_numeric) {
this->on_data_parsed_(obis_code, float_val, str_val, is_numeric);
};
size_t parsed_objects = this->parser_->parse(this->rx_buffer_.get(), this->rx_buffer_len_, callback, this->show_log_);
if (this->show_log_) {
ESP_LOGD(TAG, "PUSH data parsing complete: %zu objects, bytes consumed %zu/%zu", parsed_objects, this->rx_buffer_len_, this->rx_buffer_len_);
}
this->rx_buffer_len_ = 0;
}
void DlmsPushComponent::on_data_parsed_(const std::string &obis_code, float float_val, const std::string &str_val, bool is_numeric) {
int updated_count = 0;
#ifdef USE_SENSOR
if (is_numeric) {
for (const auto &entry : this->sensors_) {
if (entry.obis == obis_code) {
if (this->show_log_) {
ESP_LOGD(TAG, "Found sensor for OBIS code %s: '%s'", obis_code.c_str(), entry.sensor->get_name().c_str());
ESP_LOGD(TAG, "Publishing data");
}
entry.sensor->publish_state(float_val);
updated_count++;
}
}
}
#endif
#ifdef USE_TEXT_SENSOR
for (const auto &entry : this->text_sensors_) {
if (entry.obis == obis_code) {
if (this->show_log_) {
ESP_LOGD(TAG, "Found sensor for OBIS code %s: '%s'", obis_code.c_str(), entry.sensor->get_name().c_str());
ESP_LOGD(TAG, "Publishing data");
}
entry.sensor->publish_state(str_val);
updated_count++;
}
}
#endif
#ifdef USE_BINARY_SENSOR
if (is_numeric) {
bool state = float_val != 0.0f;
for (const auto &entry : this->binary_sensors_) {
if (entry.obis == obis_code) {
if (this->show_log_) {
ESP_LOGD(TAG, "Found sensor for OBIS code %s: '%s'", obis_code.c_str(), entry.sensor->get_name().c_str());
ESP_LOGD(TAG, "Publishing data");
}
entry.sensor->publish_state(state);
updated_count++;
}
}
}
#endif
if (this->show_log_ && updated_count == 0) {
ESP_LOGV(TAG, "Received OBIS %s, but no sensors are registered for it.", obis_code.c_str());
}
}
#ifdef USE_SENSOR
void DlmsPushComponent::register_sensor(const std::string &obis_code, sensor::Sensor *sensor) {
this->sensors_.push_back({obis_code, sensor});
}
#endif
#ifdef USE_TEXT_SENSOR
void DlmsPushComponent::register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor) {
this->text_sensors_.push_back({obis_code, sensor});
}
#endif
#ifdef USE_BINARY_SENSOR
void DlmsPushComponent::register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor) {
this->binary_sensors_.push_back({obis_code, sensor});
}
#endif
} // namespace dlms_push
} // namespace esphome

View File

@ -0,0 +1,91 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include <vector>
#include <map>
#include <string>
#include <array>
namespace esphome {
namespace dlms_push {
class DlmsParser;
class DlmsPushComponent : public Component, public uart::UARTDevice {
public:
DlmsPushComponent();
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_receive_timeout(uint32_t timeout_ms) { this->receive_timeout_ms_ = timeout_ms; }
void set_show_log(bool show_log) { this->show_log_ = show_log; }
void set_custom_pattern(const std::string &pattern) { this->custom_pattern_ = pattern; }
#ifdef USE_SENSOR
void register_sensor(const std::string &obis_code, sensor::Sensor *sensor);
#endif
#ifdef USE_TEXT_SENSOR
void register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor);
#endif
#ifdef USE_BINARY_SENSOR
void register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor);
#endif
protected:
void read_rx_buffer_();
void process_frame_();
void on_data_parsed_(const std::string &obis_code, float float_val, const std::string &str_val, bool is_numeric);
uint32_t receive_timeout_ms_{50};
bool show_log_{false};
std::string custom_pattern_{""};
static const size_t MAX_RX_BUFFER_SIZE = 2048;
std::unique_ptr<uint8_t[]> rx_buffer_;
size_t rx_buffer_len_{0};
uint32_t last_rx_char_time_{0};
bool receiving_{false};
DlmsParser *parser_{nullptr};
#ifdef USE_SENSOR
struct NumericSensorEntry {
std::string obis;
sensor::Sensor *sensor;
};
std::vector<NumericSensorEntry> sensors_;
#endif
#ifdef USE_TEXT_SENSOR
struct TextSensorEntry {
std::string obis;
text_sensor::TextSensor *sensor;
};
std::vector<TextSensorEntry> text_sensors_;
#endif
#ifdef USE_BINARY_SENSOR
struct BinarySensorEntry {
std::string obis;
binary_sensor::BinarySensor *sensor;
};
std::vector<BinarySensorEntry> binary_sensors_;
#endif
};
} // namespace dlms_push
} // namespace esphome

View File

@ -0,0 +1,26 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from . import DlmsPushComponent, obis_code
DEPENDENCIES = ["dlms_push"]
CONF_DLMS_PUSH_ID = "dlms_push_id"
CONF_OBIS_CODE = "obis_code"
CONFIG_SCHEMA = sensor.sensor_schema().extend(
{
cv.GenerateID(CONF_DLMS_PUSH_ID): cv.use_id(DlmsPushComponent),
cv.Required(CONF_OBIS_CODE): obis_code,
}
)
async def to_code(config):
# Retrieve the hub component
hub = await cg.get_variable(config[CONF_DLMS_PUSH_ID])
# Create the standard ESPHome sensor
var = await sensor.new_sensor(config)
# Register the sensor with the hub using its OBIS code
cg.add(hub.register_sensor(config[CONF_OBIS_CODE], var))

View File

@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from . import DlmsPushComponent, obis_code
DEPENDENCIES = ["dlms_push"]
CONF_DLMS_PUSH_ID = "dlms_push_id"
CONF_OBIS_CODE = "obis_code"
CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend(
{
cv.GenerateID(CONF_DLMS_PUSH_ID): cv.use_id(DlmsPushComponent),
cv.Required(CONF_OBIS_CODE): obis_code,
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_PUSH_ID])
var = await text_sensor.new_text_sensor(config)
cg.add(hub.register_text_sensor(config[CONF_OBIS_CODE], var))

View File

@ -1,5 +1,5 @@
esphome: esphome:
name: "esp32c3-2" name: "smartmeter"
friendly_name: SmartMeter friendly_name: SmartMeter
min_version: 2025.9.0 min_version: 2025.9.0
name_add_mac_suffix: false name_add_mac_suffix: false
@ -8,10 +8,10 @@ esp32:
variant: esp32c3 variant: esp32c3
framework: framework:
type: esp-idf type: esp-idf
version: 5.4.1
# Enable logging # Enable logging
logger: logger:
baud_rate: 0
# Enable Home Assistant API # Enable Home Assistant API
api: api:
@ -26,34 +26,40 @@ wifi:
external_components: external_components:
- source: github://Tomer27cz/xt211 - source: github://Tomer27cz/xt211
components: [xt211] components: [dlms_push]
refresh: 1s refresh: 1s
dlms_push:
id: my_dlms_meter
uart_id: bus_1
uart:
id: bus_1
rx_pin:
number: GPIO21
mode:
input: true
pullup: true
tx_pin: GPIO20
baud_rate: 9600
data_bits: 8
parity: NONE
stop_bits: 1
time:
- platform: homeassistant
id: homeassistant_time
switch: switch:
- platform: gpio - platform: gpio
pin: GPIO4 pin: GPIO4
id: indicator_led id: indicator_led
internal: True internal: True
time:
- platform: homeassistant
id: homeassistant_time
uart:
id: bus_1
rx_pin: GPIO21
tx_pin: GPIO20
baud_rate: 9600
data_bits: 8
parity: NONE
stop_bits: 1
xt211:
number: number:
- platform: template - platform: template
id: select_pulse_rate id: select_pulse_rate
name: 'Puls rate - imp/kWh' name: 'Puls rate - impkWh'
optimistic: true optimistic: true
mode: box mode: box
min_value: 100 min_value: 100
@ -67,40 +73,61 @@ button:
name: "Restart" name: "Restart"
text_sensor: text_sensor:
- platform: xt211 - platform: wifi_info
ip_address:
name: "ZZ - IP Address"
ssid:
name: "ZZ - SSID"
bssid:
name: "ZZ - BSSID"
mac_address:
name: "ZZ - Address"
dns_address:
name: "ZZ - DNS Address"
- platform: dlms_push
name: "Serial number" name: "Serial number"
obis_code: 0.0.96.1.1.255 obis_code: 0.0.96.1.1.255
entity_category: diagnostic entity_category: diagnostic
- platform: xt211 - platform: dlms_push
name: "Limmiter" name: "Limmiter"
obis_code: 0.0.17.0.0.255 obis_code: 0.0.17.0.0.255
entity_category: diagnostic entity_category: diagnostic
- platform: xt211 - platform: dlms_push
name: "Current tariff" name: "Current tariff"
obis_code: 0.0.96.14.0.255 obis_code: 0.0.96.14.0.255
entity_category: diagnostic entity_category: diagnostic
binary_sensor: binary_sensor:
- platform: xt211 - platform: status
name: "ZZ - Status"
- platform: dlms_push
name: "Disconnector state" name: "Disconnector state"
obis_code: 0.0.96.3.10.255 obis_code: 0.0.96.3.10.255
- platform: xt211 - platform: dlms_push
name: "Relay 1" name: "Relay 1"
obis_code: 0.1.96.3.10.255 obis_code: 0.1.96.3.10.255
- platform: xt211 - platform: dlms_push
name: "Relay 2" name: "Relay 2"
obis_code: 0.2.96.3.10.255 obis_code: 0.2.96.3.10.255
- platform: xt211 - platform: dlms_push
name: "Relay 3" name: "Relay 3"
obis_code: 0.3.96.3.10.255 obis_code: 0.3.96.3.10.255
- platform: xt211 - platform: dlms_push
name: "Relay 4" name: "Relay 4"
obis_code: 0.4.96.3.10.255 obis_code: 0.4.96.3.10.255
sensor: sensor:
- platform: uptime
name: 'ZZ - Uptime'
update_interval: 60s
- platform: wifi_signal - platform: wifi_signal
name: "WiFi Signal" name: "ZZ - WiFi Signal"
update_interval: 60s
- platform: internal_temperature
name: "ZZ - CPU Temp"
update_interval: 60s update_interval: 60s
- platform: template - platform: template
@ -156,7 +183,7 @@ sensor:
# filter out impossible numbers # filter out impossible numbers
- filter_out: NaN - filter_out: NaN
- platform: xt211 - platform: dlms_push
id: active_energy_consumed id: active_energy_consumed
name: "Energy" name: "Energy"
obis_code: 1.0.1.8.0.255 obis_code: 1.0.1.8.0.255
@ -167,7 +194,7 @@ sensor:
filters: filters:
- lambda: "return x/1000.0;" - lambda: "return x/1000.0;"
- platform: xt211 - platform: dlms_push
id: active_energy_consumed_t1 id: active_energy_consumed_t1
name: "Energy T1" name: "Energy T1"
obis_code: 1.0.1.8.1.255 obis_code: 1.0.1.8.1.255
@ -177,7 +204,7 @@ sensor:
state_class: total_increasing state_class: total_increasing
filters: filters:
- lambda: "return x/1000.0;" - lambda: "return x/1000.0;"
- platform: xt211 - platform: dlms_push
id: active_energy_consumed_t2 id: active_energy_consumed_t2
name: "Energy T2" name: "Energy T2"
obis_code: 1.0.1.8.2.255 obis_code: 1.0.1.8.2.255
@ -187,7 +214,7 @@ sensor:
state_class: total_increasing state_class: total_increasing
filters: filters:
- lambda: "return x/1000.0;" - lambda: "return x/1000.0;"
- platform: xt211 - platform: dlms_push
id: active_energy_consumed_t3 id: active_energy_consumed_t3
name: "Energy T3" name: "Energy T3"
obis_code: 1.0.1.8.3.255 obis_code: 1.0.1.8.3.255
@ -197,7 +224,7 @@ sensor:
state_class: total_increasing state_class: total_increasing
filters: filters:
- lambda: "return x/1000.0;" - lambda: "return x/1000.0;"
- platform: xt211 - platform: dlms_push
id: active_energy_consumed_t4 id: active_energy_consumed_t4
name: "Energy T4" name: "Energy T4"
obis_code: 1.0.1.8.4.255 obis_code: 1.0.1.8.4.255
@ -208,7 +235,7 @@ sensor:
filters: filters:
- lambda: "return x/1000.0;" - lambda: "return x/1000.0;"
- platform: xt211 - platform: dlms_push
id: active_power id: active_power
name: "Active power consumption" name: "Active power consumption"
obis_code: 1.0.1.7.0.255 obis_code: 1.0.1.7.0.255
@ -222,7 +249,7 @@ sensor:
id: power_consumption id: power_consumption
state: !lambda 'return x;' state: !lambda 'return x;'
- platform: xt211 - platform: dlms_push
id: active_power_l1 id: active_power_l1
name: "Active power consumption L1" name: "Active power consumption L1"
obis_code: 1.0.21.7.0.255 obis_code: 1.0.21.7.0.255
@ -230,7 +257,7 @@ sensor:
accuracy_decimals: 0 accuracy_decimals: 0
device_class: power device_class: power
state_class: measurement state_class: measurement
- platform: xt211 - platform: dlms_push
id: active_power_l2 id: active_power_l2
name: "Active power consumption L2" name: "Active power consumption L2"
obis_code: 1.0.41.7.0.255 obis_code: 1.0.41.7.0.255
@ -238,7 +265,7 @@ sensor:
accuracy_decimals: 0 accuracy_decimals: 0
device_class: power device_class: power
state_class: measurement state_class: measurement
- platform: xt211 - platform: dlms_push
id: active_power_l3 id: active_power_l3
name: "Active power consumption L3" name: "Active power consumption L3"
obis_code: 1.0.61.7.0.255 obis_code: 1.0.61.7.0.255
@ -247,7 +274,7 @@ sensor:
device_class: power device_class: power
state_class: measurement state_class: measurement
- platform: xt211 - platform: dlms_push
id: active_power_delivery id: active_power_delivery
name: "Active power delivery" name: "Active power delivery"
obis_code: 1.0.2.7.0.255 obis_code: 1.0.2.7.0.255
@ -256,7 +283,7 @@ sensor:
device_class: power device_class: power
state_class: measurement state_class: measurement
- platform: xt211 - platform: dlms_push
id: active_power_l1_delivery id: active_power_l1_delivery
name: "Active power L1 delivery" name: "Active power L1 delivery"
obis_code: 1.0.22.7.0.255 obis_code: 1.0.22.7.0.255
@ -264,7 +291,7 @@ sensor:
accuracy_decimals: 0 accuracy_decimals: 0
device_class: power device_class: power
state_class: measurement state_class: measurement
- platform: xt211 - platform: dlms_push
id: active_power_l2_delivery id: active_power_l2_delivery
name: "Active power L2 delivery" name: "Active power L2 delivery"
obis_code: 1.0.42.7.0.255 obis_code: 1.0.42.7.0.255
@ -272,7 +299,7 @@ sensor:
accuracy_decimals: 0 accuracy_decimals: 0
device_class: power device_class: power
state_class: measurement state_class: measurement
- platform: xt211 - platform: dlms_push
id: active_power_l3_delivery id: active_power_l3_delivery
name: "Active power L3 delivery" name: "Active power L3 delivery"
obis_code: 1.0.62.7.0.255 obis_code: 1.0.62.7.0.255