esphome/dlms_parser

1.2.0

Latest
uploaded 1 day ago
A C++20 library for parsing DLMS/COSEM push telegrams from electricity meters. It is designed for embedded and integration-heavy environments such as ESPHome

Readme

# dlms_parser

`dlms_parser` is a C++20 library for parsing DLMS/COSEM push telegrams from electricity meters.<br>
It is designed for embedded and integration-heavy environments such as ESPHome, but it also works on desktop platforms.

## Features

- **Transport decoding**: `RAW`, `HDLC`, `M-Bus`. Auto-detects the frame format based on the leading byte. Includes multi-frame segmentation and General Block Transfer
- **Encryption**: AES-128-GCM decryption and optional authentication tag verification for `General-GLO-Ciphering` and `General-DED-Ciphering` APDUs
- **Crypto Backends**: Pluggable decryption backends with built-in support for `mbedTLS`, `BearSSL`, and `TF-PSA`
- **Pattern matching**: DSL-based AXDR descriptor patterns with built-in presets and custom registration
- **Callback API**: cooked callback delivers OBIS code + scaled value
- **Embedded-friendly**: no heap allocation during parsing
- **Portable**: builds on ESP32 (IDF/Arduino), ESP8266, Linux, macOS, Windows

## How to use

Complete example with the explanation: [test_example.cpp](https://github.com/esphome-libs/dlms_parser/blob/main/tests/test_example.cpp)

### Creating custom patterns to match your meter's telegram structure

The parser starts with no registered AXDR patterns. Load the built-ins first unless you want full control:
```c++
parser.load_default_patterns();
```

**Built-in patterns (available after calling `parser.load_default_patterns()`):**

| Name                                     | Pattern          | Priority |
|------------------------------------------|------------------|---------:|
| `SelfDescribing`                         | `SelfDesc`       |       10 |
| `classId-taggedObis-scaler-value`        | `TC,TO,TS,TV`    |       20 |
| `taggedObis-value-scalerUnit`            | `TO,TV,TSU`      |       30 |
| `value-classId-scalerUnit-taggedObis`    | `TV,TC,TSU,TO`   |       40 |
| `zpaAidon-untaggedLayout`                | `ADV`            |       50 |
| `structuredObis-value-scalerUnit`        | `S(TO, TV, TSU)` |       60 |
| `structuredObis-value`                   | `S(TO, TV)`      |       70 |
| `flatObis-valuePair`                     | `TO, TV`         |       80 |
| `firstElement-dateTime`                  | `F, S(TO, TDTM)` |       90 |
| `swappedTagObis-value-scalerUnit`        | `TOW, TV, TSU`   |      100 |

**Registering Custom Patterns:**

If your meter emits a layout not covered by the built-ins, you can register custom patterns. Lower priority numbers are evaluated first.
```c++
// Simple — priority 0 (tried before built-ins)
parser.register_pattern("TC, TO, TDTM");

// Named with explicit priority
parser.register_pattern("MyPattern", "TO, TV, S(TS, TU)", 5);

// With default OBIS — used when the pattern captures no OBIS code
const uint8_t meter_obis[] = {0, 0, 96, 1, 0, 255};  // 0.0.96.1.0.255
parser.register_pattern("MeterID", "L, TSTR", 0, meter_obis);
```

Pattern priority matters:

- lower priority number is tried first
- `register_pattern(dsl)` uses priority `0`
- built-ins start at priority `10`

Common examples:

```cpp
parser.register_pattern("TC, TO, TDTM");          // datetime value
parser.register_pattern("C, O, A, V, TS, TU");    // untagged flat
parser.register_pattern("TO, TV, S(TS, TU)");     // tagged with scaler-unit
parser.register_pattern("TO, TV");                // flat OBIS + value pairs (no scaler)
parser.register_pattern("L, TSTR");               // last element as string
parser.register_pattern("TOW, TV, TSU");          // Landis+Gyr swapped OBIS
```

### Token reference
| Token          | Meaning                                        | Hex example                     |
|----------------|------------------------------------------------|---------------------------------|
| `SelfDesc`     | array of value descriptions followed by values | definitions array + values      |
| `F`            | first element guard                            | position check only             |
| `L`            | last element guard                             | position check only             |
| `C`            | class ID, 2-byte uint16 without tag            | `00 03`                         |
| `TC`           | tagged class ID                                | `12 00 03`                      |
| `O`            | OBIS code, 6-byte octet string without tag     | `01 00 01 08 00 FF`             |
| `TO`           | tagged OBIS code                               | `09 06 01 00 01 08 00 FF`       |
| `TOW`          | tagged OBIS with swapped tag bytes             | `06 09 01 00 1F 07 00 FF`       |
| `A`            | attribute index, 1-byte uint8 without tag      | `02`                            |
| `TA`           | tagged attribute                               | `11 02` or `0F 02`              |
| `V` / `TV`     | generic value                                  | `06 00 00 07 A4`                |
| `TSTR`         | tagged string-like value                       | `09 08 38 34 38 39 35 31 32 36` |
| `TDTM`         | tagged 12-byte date-time value                 | `19 ...` or `09 0C ...`         |
| `TS`           | tagged scaler                                  | `0F FF`                         |
| `TU`           | tagged unit enum                               | `16 23`                         |
| `TSU`          | tagged scaler-unit pair                        | `02 02 0F FF 16 23`             |
| `S(x, y, ...)` | inline sub-structure                           | `02 03`                         |
| `DN`           | descend into nested structure                  | control token                   |
| `UP`           | return from nested structure                   | control token                   |

## API Reference

### `DlmsParser` Core Methods

> **⚠️ Warning:** If you intend to use encryption, you **must** provide a concrete `Aes128GcmDecryptor` backend to the constructor before calling `set_decryption_key` or `set_authentication_key`. Calling these methods on a parser initialized with the default `nullptr` decryptor will cause a null pointer dereference.

| Method                                                               | Description                                                                                           |
|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|
| `DlmsParser(Aes128GcmDecryptor* = nullptr)`                          | Constructor accepting an optional pointer to an AES-128-GCM decryptor backend.                        |
| `set_skip_crc_check(bool)`                                           | Skip CRC/checksum validation for HDLC and M-Bus.                                                      |
| `set_decryption_key(const Aes128GcmDecryptionKey&)`                  | Set AES-128-GCM decryption key (GUEK). **Requires a non-null decryptor.**                             |
| `set_authentication_key(const Aes128GcmAuthenticationKey&)`          | Set AES-128-GCM authentication key (GAK) for GCM tag verification. **Requires a non-null decryptor.** |
| `load_default_patterns()`                                            | Register all built-in patterns (T1, T2, T3, DateTime, etc.).                                          |
| `ParseResult parse(std::span<uint8_t> buf, const DlmsDataCallback&)` | Parse a complete frame; modifies the buffer in-place and triggers the callback.                       |
### Supported APDU Tags

Common APDU tags accepted by the parser:

| Byte   | Meaning                                                                             |
|--------|-------------------------------------------------------------------------------------|
| `0x0F` | `DATA-NOTIFICATION`                                                                 |
| `0xE0` | `General-Block-Transfer` — reassembles numbered blocks, then re-enters APDU parsing |
| `0xDB` | `General-GLO-Ciphering` — encrypted, needs decryption key                           |
| `0xDF` | `General-DED-Ciphering` — encrypted, needs decryption key                           |
| `0x01` | raw AXDR array                                                                      |
| `0x02` | raw AXDR structure                                                                  |

### Basic Example
```c++
#include <vector>
#include "dlms_parser.h"
#include "decryption/aes_128_gcm_decryptor_mbedtls.h"

using namespace dlms_parser;

int main() {
    // 1. Initialize a crypto backend (e.g., mbedTLS)
    Aes128GcmDecryptorMbedTls decryptor;
    
    // 2. Initialize the parser with a pointer to the decryptor
    DlmsParser parser(&decryptor);
    
    // 3. Set keys using the robust hex loader
    auto dec_key = Aes128GcmDecryptionKey::from_hex("00112233445566778899AABBCCDDEEFF");
    auto auth_key = Aes128GcmAuthenticationKey::from_hex("FFEEDDCCBBAA99887766554433221100");
    
    if (dec_key) parser.set_decryption_key(*dec_key);
    if (auth_key) parser.set_authentication_key(*auth_key);

    // 4. Load common built-in meter layout patterns
    parser.load_default_patterns();

    // 5. Provide your data (will be modified in-place during parsing)
    std::vector<uint8_t> my_telegram = { /* ... byte data ... */ };
    
    // 6. Define your callback
    auto callback = [](const char* obis, float f_val, const char* s_val, bool is_numeric) {
        printf("Matched OBIS: %s | String: %s | Float: %f\n", obis, s_val, f_val);
    };

    // 7. Parse the telegram by explicitly constructing a std::span<uint8_t>
    ParseResult result = parser.parse(std::span<uint8_t>(my_telegram.data(), my_telegram.size()), callback);
    
    printf("Successfully parsed %zu COSEM objects!\n", result.count);
    return 0;
}
```

## Logging

`dlms_parser` includes a built-in logging system that is useful for debugging frame parsing and pattern matching. You can hook into it by providing a custom log function:
```c++
#include "log.h"
#include <cstdarg>
#include <cstdio>

// ... inside your setup code ...
dlms_parser::Logger::set_log_function([](dlms_parser::LogLevel level, const char* fmt, va_list args) {
    // Implement your platform-specific print here
    vprintf(fmt, args);
    printf("\n");
});
```

## How to add the library to your project

### PlatformIO package
[https://registry.platformio.org/libraries/esphome/dlms_parser](https://registry.platformio.org/libraries/esphome/dlms_parser)

### ESP-IDF component
[https://components.espressif.com/components/esphome/dlms_parser](https://components.espressif.com/components/esphome/dlms_parser)

### CMake
```cmake
FetchContent_Declare(
  dlms_parser
  GIT_REPOSITORY https://github.com/esphome-libs/dlms_parser
  GIT_TAG v1.0)
FetchContent_MakeAvailable(dlms_parser)

add_executable(your_project_name main.cpp)
target_link_libraries(your_project_name PRIVATE dlms_parser)
```

### Acknowledgements

This library builds on foundational work and protocol insights from:
- [esphome-dlms-cosem](https://github.com/latonita/esphome-dlms-cosem) - original ESPHome DLMS/COSEM component and AXDR parser by **latonita**.
- [xt211](https://github.com/Tomer27cz/xt211) - Sagemcom XT211 parser by **Tomer27cz**, instrumental in de-Guruxing the protocol handling.

## References
- [DLMS/COSEM Architecture and Protocols. Green Book Edition 11](https://github.com/zhuyangfei/DLMS-green-book/blob/main/Green-Book-Ed-11-V1-0.pdf)
- [CONSUMER INFORMATION INTERFACE (CII) SPECIFICATION](https://wiki.weble.ch/articles/landys_e450_docs/customer_information_interface_(cii)_specification.pdf)
- [EWK Energie AG. Smart meter customer interface](https://ewk-energie.ch/wp-content/uploads/2025/08/smart-meter-kundenschnittstelle.pdf)
- [Zaehler Landis+Gyr E450/E570. Smart meter customer interface](https://www.bkw.ch/fileadmin/user_upload/03_Energie/03_01_Stromversorgung_Privat-_und_Gewerbekunden/Zaehlerablesung/BKW_faktenblatt_kundenschnittstelle_L_G_E450_E570_def_Web.pdf)
- [E450 Specification](https://assets.netzburgenland.at/Spezifikation_Kundenschnittstelle_E450_korr_2_009418889e.pdf)
- [Netz Smart Meter specification](https://netz-noe.at/getContentAsset/568bef9a-3bd1-4f2e-a710-6ba7e71cb746/0ee16eb8-9692-4f25-b8a4-d007b35915a4/218_15_Smart-Meter-Folder-Kundenschnittstelle-2025_0110.pdf?language=de)

Links

Supports all targets

To add this component to your project, run:

idf.py add-dependency "esphome/dlms_parser^1.2.0"

download archive

Stats

  • Archive size
    Archive size ~ 36.68 KB
  • Downloaded in total
    Downloaded in total 1.9k times
  • Weekly Downloads Weekly Downloads (All Versions)
  • Downloaded this version
    This version: 13 times

Badge

esphome/dlms_parser version: 1.2.0
|