dlms_parser is 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, but it also works on desktop platforms.
- 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-CipheringandGeneral-DED-CipheringAPDUs - Crypto Backends: Pluggable decryption backends with built-in support for
mbedTLS,BearSSL, andTF-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
Complete example with the explanation: test_example.cpp
The parser starts with no registered AXDR patterns. Load the built-ins first unless you want full control:
parser.load_default_patterns();Built-in patterns (available after calling parser.load_default_patterns()):
| Name | Pattern | Priority | Typical use |
|---|---|---|---|
classId-taggedObis-scaler-value |
TC,TO,TS,TV |
10 | class ID, tagged OBIS, scaler, value |
taggedObis-value-scalerUnit |
TO,TV,TSU |
20 | tagged OBIS, value, scaler-unit structure |
value-classId-scalerUnit-taggedObis |
TV,TC,TSU,TO |
30 | value first, class ID, scaler-unit, OBIS |
zpaAidon-untaggedLayout |
ADV |
40 | untagged ZPA/Aidon-style layouts |
structuredObis-value-scalerUnit |
S(TO, TV, TSU) |
50 | OBIS, value, scaler-unit structure |
structuredObis-value |
S(TO, TV) |
60 | OBIS and value structure |
flatObis-valuePair |
TO, TV |
70 | flat OBIS + value pairs |
firstElement-dateTime |
F, S(TO, TDTM) |
80 | first-element date-time structure |
swappedTagObis-value-scalerUnit |
TOW, TV, TSU |
90 | swapped-tag OBIS, value, scaler-unit |
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.
// 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 priority0- built-ins start at priority
10
Common examples:
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 | Meaning | Hex example |
|---|---|---|
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 |
⚠️ Warning: If you intend to use encryption, you must provide a concreteAes128GcmDecryptorbackend to the constructor before callingset_decryption_keyorset_authentication_key. Calling these methods on a parser initialized with the defaultnullptrdecryptor 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. |
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 |
#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;
}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:
#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");
});https://registry.platformio.org/libraries/esphome/dlms_parser
https://components.espressif.com/components/esphome/dlms_parser
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)This library builds on foundational work and protocol insights from:
- esphome-dlms-cosem - original ESPHome DLMS/COSEM component and AXDR parser by latonita.
- xt211 - Sagemcom XT211 parser by Tomer27cz, instrumental in de-Guruxing the protocol handling.