Compare commits
2 Commits
ffec2a901f
...
81bbd19c09
| Author | SHA1 | Date |
|---|---|---|
|
|
81bbd19c09 | |
|
|
1db8e48acf |
53
README.cz.md
53
README.cz.md
|
|
@ -138,11 +138,11 @@ Power Led (volitelné):
|
|||
|
||||
# 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))
|
||||
- `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.
|
||||
|
||||
Více detailů v projektu [esphome-dlms-cosem repository](https://github.com/latonita/esphome-dlms-cosem) od [latonita](https://github.com/latonita)
|
||||
- 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.
|
||||
|
||||
## ESPHome konfigurace
|
||||
|
||||
|
|
@ -160,34 +160,57 @@ Přidej externí komponentu:
|
|||
```yaml
|
||||
external_components:
|
||||
- source: github://Tomer27cz/xt211
|
||||
components: [xt211]
|
||||
components: [dlms_push]
|
||||
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
|
||||
logger:
|
||||
baud_rate: 0
|
||||
|
||||
uart:
|
||||
id: bus_1
|
||||
rx_pin:
|
||||
number: GPIO21
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
tx_pin: GPIO20
|
||||
rx_pin: GPIO21
|
||||
baud_rate: 9600
|
||||
data_bits: 8
|
||||
parity: NONE
|
||||
stop_bits: 1
|
||||
|
||||
xt211:
|
||||
push_show_log: true
|
||||
dlms_push:
|
||||
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`)
|
||||
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
|
||||
sensor:
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_energy_consumed
|
||||
name: "Energy"
|
||||
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
|
||||
binary_sensor:
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Relay 1"
|
||||
obis_code: 0.1.96.3.10.255
|
||||
```
|
||||
|
|
@ -213,7 +236,7 @@ binary_sensor:
|
|||
|
||||
```yaml
|
||||
text_sensor:
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Serial number"
|
||||
obis_code: 0.0.96.1.1.255
|
||||
entity_category: diagnostic
|
||||
|
|
@ -301,7 +324,7 @@ sensor:
|
|||
|
||||
[... pulzní měřič z výše uvedeného configu ...]
|
||||
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power
|
||||
name: "Active power consumption"
|
||||
obis_code: 1.0.1.7.0.255
|
||||
|
|
|
|||
42
README.md
42
README.md
|
|
@ -140,11 +140,11 @@ Power Led (optional):
|
|||
|
||||
# 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.
|
||||
|
||||
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)
|
||||
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.
|
||||
|
||||
## ESPHome configuration
|
||||
|
||||
|
|
@ -164,34 +164,48 @@ Add the external component to your ESPHome configuration:
|
|||
```yaml
|
||||
external_components:
|
||||
- source: github://Tomer27cz/xt211
|
||||
components: [xt211]
|
||||
components: [dlms_push]
|
||||
refresh: 1s
|
||||
```
|
||||
Then configure the DLMS/Cosem component:
|
||||
- push_show_log: true (optional, for debugging purposes - shows all received PUSH messages in the log)
|
||||
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
|
||||
|
||||
Disable the log onece everything is working fine.
|
||||
|
||||
```yaml
|
||||
logger:
|
||||
baud_rate: 0
|
||||
|
||||
uart:
|
||||
id: bus_1
|
||||
rx_pin:
|
||||
number: GPIO21
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
tx_pin: GPIO20
|
||||
rx_pin: GPIO21
|
||||
baud_rate: 9600
|
||||
data_bits: 8
|
||||
parity: NONE
|
||||
stop_bits: 1
|
||||
|
||||
xt211:
|
||||
push_show_log: true
|
||||
dlms_push:
|
||||
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`)
|
||||
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
|
||||
sensor:
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_energy_consumed
|
||||
name: "Energy"
|
||||
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
|
||||
binary_sensor:
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Relay 1"
|
||||
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
|
||||
text_sensor:
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Serial number"
|
||||
obis_code: 0.0.96.1.1.255
|
||||
entity_category: diagnostic
|
||||
|
|
@ -306,7 +320,7 @@ sensor:
|
|||
|
||||
[... pulse meter from above ...]
|
||||
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power
|
||||
name: "Active power consumption"
|
||||
obis_code: 1.0.1.7.0.255
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
esphome:
|
||||
name: "esp32c3-2"
|
||||
name: "smartmeter"
|
||||
friendly_name: SmartMeter
|
||||
min_version: 2025.9.0
|
||||
name_add_mac_suffix: false
|
||||
|
|
@ -8,10 +8,10 @@ esp32:
|
|||
variant: esp32c3
|
||||
framework:
|
||||
type: esp-idf
|
||||
version: 5.4.1
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
baud_rate: 0
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
|
|
@ -26,34 +26,40 @@ wifi:
|
|||
|
||||
external_components:
|
||||
- source: github://Tomer27cz/xt211
|
||||
components: [xt211]
|
||||
components: [dlms_push]
|
||||
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:
|
||||
- platform: gpio
|
||||
pin: GPIO4
|
||||
id: indicator_led
|
||||
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:
|
||||
- platform: template
|
||||
id: select_pulse_rate
|
||||
name: 'Puls rate - imp/kWh'
|
||||
name: 'Puls rate - imp⁄kWh'
|
||||
optimistic: true
|
||||
mode: box
|
||||
min_value: 100
|
||||
|
|
@ -67,42 +73,63 @@ button:
|
|||
name: "Restart"
|
||||
|
||||
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"
|
||||
obis_code: 0.0.96.1.1.255
|
||||
entity_category: diagnostic
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Limmiter"
|
||||
obis_code: 0.0.17.0.0.255
|
||||
entity_category: diagnostic
|
||||
entity_category: diagnostic
|
||||
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Current tariff"
|
||||
obis_code: 0.0.96.14.0.255
|
||||
entity_category: diagnostic
|
||||
|
||||
binary_sensor:
|
||||
- platform: xt211
|
||||
- platform: status
|
||||
name: "ZZ - Status"
|
||||
|
||||
- platform: dlms_push
|
||||
name: "Disconnector state"
|
||||
obis_code: 0.0.96.3.10.255
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Relay 1"
|
||||
obis_code: 0.1.96.3.10.255
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Relay 2"
|
||||
obis_code: 0.2.96.3.10.255
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Relay 3"
|
||||
obis_code: 0.3.96.3.10.255
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
name: "Relay 4"
|
||||
obis_code: 0.4.96.3.10.255
|
||||
|
||||
sensor:
|
||||
- platform: wifi_signal
|
||||
name: "WiFi Signal"
|
||||
- platform: uptime
|
||||
name: 'ZZ - Uptime'
|
||||
update_interval: 60s
|
||||
|
||||
- platform: wifi_signal
|
||||
name: "ZZ - WiFi Signal"
|
||||
update_interval: 60s
|
||||
- platform: internal_temperature
|
||||
name: "ZZ - CPU Temp"
|
||||
update_interval: 60s
|
||||
|
||||
- platform: template
|
||||
id: power_consumption
|
||||
name: "Power Consumption"
|
||||
|
|
@ -122,20 +149,20 @@ sensor:
|
|||
accuracy_decimals: 0
|
||||
pin: GPIO6
|
||||
|
||||
on_raw_value:
|
||||
on_raw_value:
|
||||
then:
|
||||
- switch.turn_on: indicator_led
|
||||
- delay: 100ms
|
||||
- switch.turn_off: indicator_led
|
||||
- switch.turn_off: indicator_led
|
||||
on_value:
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: power_consumption
|
||||
state: !lambda 'return x;'
|
||||
|
||||
|
||||
# dont know what this does but it was commented out
|
||||
internal_filter: 100ms
|
||||
|
||||
|
||||
filters:
|
||||
# Sensor can quickly transition between on and off (unintendet bevaior)
|
||||
# this meter has max load of 13.27 kW (32A - 3 Phase - 240V) = pulse every 270ms at full load
|
||||
|
|
@ -152,11 +179,11 @@ sensor:
|
|||
# https://github.com/klaasnicolaas/home-assistant-glow/#reduce-the-amount-of-data-the-sensors-produce
|
||||
# for more information.
|
||||
#- throttle_average: 10s
|
||||
|
||||
|
||||
# filter out impossible numbers
|
||||
- filter_out: NaN
|
||||
|
||||
- platform: xt211
|
||||
|
||||
- platform: dlms_push
|
||||
id: active_energy_consumed
|
||||
name: "Energy"
|
||||
obis_code: 1.0.1.8.0.255
|
||||
|
|
@ -167,7 +194,7 @@ sensor:
|
|||
filters:
|
||||
- lambda: "return x/1000.0;"
|
||||
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_energy_consumed_t1
|
||||
name: "Energy T1"
|
||||
obis_code: 1.0.1.8.1.255
|
||||
|
|
@ -177,7 +204,7 @@ sensor:
|
|||
state_class: total_increasing
|
||||
filters:
|
||||
- lambda: "return x/1000.0;"
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_energy_consumed_t2
|
||||
name: "Energy T2"
|
||||
obis_code: 1.0.1.8.2.255
|
||||
|
|
@ -187,7 +214,7 @@ sensor:
|
|||
state_class: total_increasing
|
||||
filters:
|
||||
- lambda: "return x/1000.0;"
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_energy_consumed_t3
|
||||
name: "Energy T3"
|
||||
obis_code: 1.0.1.8.3.255
|
||||
|
|
@ -197,7 +224,7 @@ sensor:
|
|||
state_class: total_increasing
|
||||
filters:
|
||||
- lambda: "return x/1000.0;"
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_energy_consumed_t4
|
||||
name: "Energy T4"
|
||||
obis_code: 1.0.1.8.4.255
|
||||
|
|
@ -208,7 +235,7 @@ sensor:
|
|||
filters:
|
||||
- lambda: "return x/1000.0;"
|
||||
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power
|
||||
name: "Active power consumption"
|
||||
obis_code: 1.0.1.7.0.255
|
||||
|
|
@ -222,7 +249,7 @@ sensor:
|
|||
id: power_consumption
|
||||
state: !lambda 'return x;'
|
||||
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power_l1
|
||||
name: "Active power consumption L1"
|
||||
obis_code: 1.0.21.7.0.255
|
||||
|
|
@ -230,7 +257,7 @@ sensor:
|
|||
accuracy_decimals: 0
|
||||
device_class: power
|
||||
state_class: measurement
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power_l2
|
||||
name: "Active power consumption L2"
|
||||
obis_code: 1.0.41.7.0.255
|
||||
|
|
@ -238,7 +265,7 @@ sensor:
|
|||
accuracy_decimals: 0
|
||||
device_class: power
|
||||
state_class: measurement
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power_l3
|
||||
name: "Active power consumption L3"
|
||||
obis_code: 1.0.61.7.0.255
|
||||
|
|
@ -246,8 +273,8 @@ sensor:
|
|||
accuracy_decimals: 0
|
||||
device_class: power
|
||||
state_class: measurement
|
||||
|
||||
- platform: xt211
|
||||
|
||||
- platform: dlms_push
|
||||
id: active_power_delivery
|
||||
name: "Active power delivery"
|
||||
obis_code: 1.0.2.7.0.255
|
||||
|
|
@ -256,7 +283,7 @@ sensor:
|
|||
device_class: power
|
||||
state_class: measurement
|
||||
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power_l1_delivery
|
||||
name: "Active power L1 delivery"
|
||||
obis_code: 1.0.22.7.0.255
|
||||
|
|
@ -264,7 +291,7 @@ sensor:
|
|||
accuracy_decimals: 0
|
||||
device_class: power
|
||||
state_class: measurement
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power_l2_delivery
|
||||
name: "Active power L2 delivery"
|
||||
obis_code: 1.0.42.7.0.255
|
||||
|
|
@ -272,7 +299,7 @@ sensor:
|
|||
accuracy_decimals: 0
|
||||
device_class: power
|
||||
state_class: measurement
|
||||
- platform: xt211
|
||||
- platform: dlms_push
|
||||
id: active_power_l3_delivery
|
||||
name: "Active power L3 delivery"
|
||||
obis_code: 1.0.62.7.0.255
|
||||
|
|
|
|||
Loading…
Reference in New Issue