ESPHome 2026.3.3
Loading...
Searching...
No Matches
dlms_meter.cpp
Go to the documentation of this file.
1#include "dlms_meter.h"
2
3#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
4#include <bearssl/bearssl.h>
5#elif defined(USE_ESP32)
6#include "mbedtls/esp_config.h"
7#include "mbedtls/gcm.h"
8#endif
9
10namespace esphome::dlms_meter {
11
12static constexpr const char *TAG = "dlms_meter";
13
15 const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic";
16 ESP_LOGCONFIG(TAG,
17 "DLMS Meter:\n"
18 " Provider: %s\n"
19 " Read Timeout: %u ms",
20 provider_name, this->read_timeout_);
21#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_);
22 DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, )
23#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_);
24 DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, )
25}
26
28 // Read while data is available, netznoe uses two frames so allow 2x max frame length
29 size_t avail = this->available();
30 if (avail > 0) {
31 size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
32 if (remaining == 0) {
33 ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
34 } else {
35 // Read all available bytes in batches to reduce UART call overhead.
36 // Cap reads to remaining buffer capacity.
37 if (avail > remaining) {
38 avail = remaining;
39 }
40 uint8_t buf[64];
41 while (avail > 0) {
42 size_t to_read = std::min(avail, sizeof(buf));
43 if (!this->read_array(buf, to_read)) {
44 break;
45 }
46 avail -= to_read;
47 this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
48 this->last_read_ = millis();
49 }
50 }
51 }
52
53 if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
54 this->mbus_payload_.clear();
55 if (!this->parse_mbus_(this->mbus_payload_))
56 return;
57
58 uint16_t message_length;
59 uint8_t systitle_length;
60 uint16_t header_offset;
61 if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset))
62 return;
63
64 if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) {
65 ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length);
66 this->receive_buffer_.clear();
67 return;
68 }
69
70 // Decrypt in place and then decode the OBIS codes
71 if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset))
72 return;
73 this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length);
74 }
75}
76
77bool DlmsMeterComponent::parse_mbus_(std::vector<uint8_t> &mbus_payload) {
78 ESP_LOGV(TAG, "Parsing M-Bus frames");
79 uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames
80
81 while (frame_offset < this->receive_buffer_.size()) {
82 // Ensure enough bytes remain for the minimal intro header before accessing indices
83 if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) {
84 ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH,
85 (this->receive_buffer_.size() - frame_offset));
86 this->receive_buffer_.clear();
87 return false;
88 }
89
90 // Check start bytes
91 if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME ||
92 this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) {
93 ESP_LOGE(TAG, "MBUS: Start bytes do not match");
94 this->receive_buffer_.clear();
95 return false;
96 }
97
98 // Both length bytes must be identical
99 if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] !=
100 this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) {
101 ESP_LOGE(TAG, "MBUS: Length bytes do not match");
102 this->receive_buffer_.clear();
103 return false;
104 }
105
106 uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame
107
108 // Check if received data is enough for the given frame length
109 if (this->receive_buffer_.size() - frame_offset <
110 frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte
111 ESP_LOGE(TAG, "MBUS: Frame too big for received data");
112 this->receive_buffer_.clear();
113 return false;
114 }
115
116 // Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte
117 size_t required_total =
118 frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes
119 if (this->receive_buffer_.size() - frame_offset < required_total) {
120 ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total,
121 this->receive_buffer_.size() - frame_offset);
122 this->receive_buffer_.clear();
123 return false;
124 }
125
126 if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] !=
127 STOP_BYTE) {
128 ESP_LOGE(TAG, "MBUS: Invalid stop byte");
129 this->receive_buffer_.clear();
130 return false;
131 }
132
133 // Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte
134 uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored
135 for (uint16_t i = 0; i < frame_length; i++) {
136 checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i];
137 }
138 if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) {
139 ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum,
140 this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]);
141 this->receive_buffer_.clear();
142 return false;
143 }
144
145 mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH],
146 &this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]);
147
148 frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH;
149 }
150 return true;
151}
152
153bool DlmsMeterComponent::parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length,
154 uint8_t &systitle_length, uint16_t &header_offset) {
155 ESP_LOGV(TAG, "Parsing DLMS header");
156 if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) {
157 ESP_LOGE(TAG, "DLMS: Payload too short");
158 this->receive_buffer_.clear();
159 return false;
160 }
161
162 if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB)
163 ESP_LOGE(TAG, "DLMS: Unsupported cipher");
164 this->receive_buffer_.clear();
165 return false;
166 }
167
168 systitle_length = mbus_payload[DLMS_SYST_OFFSET];
169
170 if (systitle_length != 0x08) { // Only system titles with length of 8 are supported
171 ESP_LOGE(TAG, "DLMS: Unsupported system title length");
172 this->receive_buffer_.clear();
173 return false;
174 }
175
176 message_length = mbus_payload[DLMS_LENGTH_OFFSET];
177 header_offset = 0;
178
179 if (this->provider_ == PROVIDER_NETZNOE) {
180 // for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next
181 // byte. Check some bytes to see if received data still matches expectation
182 if (message_length == NETZ_NOE_MAGIC_BYTE &&
183 mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH &&
184 mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) {
185 message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1];
186 header_offset = 1;
187 } else {
188 ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN");
189 }
190 } else {
191 if (message_length == TWO_BYTE_LENGTH) {
192 message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]);
193 header_offset = DLMS_HEADER_EXT_OFFSET;
194 }
195 }
196 if (message_length < DLMS_LENGTH_CORRECTION) {
197 ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length);
198 this->receive_buffer_.clear();
199 return false;
200 }
201 message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length
202
203 if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) {
204 ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(),
205 DLMS_HEADER_LENGTH, header_offset, message_length);
206 ESP_LOGE(TAG, "DLMS: Message has invalid length");
207 this->receive_buffer_.clear();
208 return false;
209 }
210
211 if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 &&
212 mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] !=
213 0x20) { // Only certain security suite is supported (0x21 || 0x20)
214 ESP_LOGE(TAG, "DLMS: Unsupported security control byte");
215 this->receive_buffer_.clear();
216 return false;
217 }
218
219 return true;
220}
221
222bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
223 uint16_t header_offset) {
224 ESP_LOGV(TAG, "Decrypting payload");
225 uint8_t iv[12]; // Reserve space for the IV, always 12 bytes
226 // Copy system title to IV (System title is before length; no header offset needed!)
227 // Add 1 to the offset in order to skip the system title length byte
228 memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length);
229 memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET],
230 DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV
231
232 uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET];
233
234#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
235 br_gcm_context gcm_ctx;
236 br_aes_ct_ctr_keys bc;
237 br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size());
238 br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32);
239 br_gcm_reset(&gcm_ctx, iv, sizeof(iv));
240 br_gcm_flip(&gcm_ctx);
241 br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
242#elif defined(USE_ESP32)
243 size_t outlen = 0;
244 mbedtls_gcm_context gcm_ctx;
245 mbedtls_gcm_init(&gcm_ctx);
246 mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8);
247 mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv));
248 auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen);
249 mbedtls_gcm_free(&gcm_ctx);
250 if (ret != 0) {
251 ESP_LOGE(TAG, "Decryption failed with error: %d", ret);
252 this->receive_buffer_.clear();
253 return false;
254 }
255#else
256#error "Invalid Platform"
257#endif
258
259 if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) {
260 ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid");
261 this->receive_buffer_.clear();
262 return false;
263 }
264 ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length);
265 return true;
266}
267
268void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) {
269 ESP_LOGV(TAG, "Decoding payload");
270 MeterData data{};
271 uint16_t current_position = DECODER_START_OFFSET;
272 bool power_factor_found = false;
273
274 while (current_position + OBIS_CODE_OFFSET <= message_length) {
275 if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) {
276 ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]);
277 this->receive_buffer_.clear();
278 return;
279 }
280
281 uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET];
282 if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) {
283 ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length);
284 this->receive_buffer_.clear();
285 return;
286 }
287 if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) {
288 ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code");
289 this->receive_buffer_.clear();
290 return;
291 }
292
293 uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET];
294 uint8_t obis_medium = obis_code[OBIS_A];
295 uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]);
296
297 bool timestamp_found = false;
298 bool meter_number_found = false;
299 if (this->provider_ == PROVIDER_NETZNOE) {
300 // Do not advance Position when reading the Timestamp at DECODER_START_OFFSET
301 if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) {
302 timestamp_found = true;
303 } else if (power_factor_found) {
304 meter_number_found = true;
305 power_factor_found = false;
306 } else {
307 current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position
308 }
309 } else {
310 current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type
311 }
312 if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY &&
313 obis_medium != Medium::ABSTRACT) {
314 ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium);
315 this->receive_buffer_.clear();
316 return;
317 }
318
319 if (current_position >= message_length) {
320 ESP_LOGE(TAG, "OBIS: Buffer too short for data type");
321 this->receive_buffer_.clear();
322 return;
323 }
324
325 float value = 0.0f;
326 uint8_t value_size = 0;
327 uint8_t data_type = plaintext[current_position];
328 current_position++;
329
330 switch (data_type) {
332 value_size = 4;
333 if (current_position + value_size > message_length) {
334 ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED");
335 this->receive_buffer_.clear();
336 return;
337 }
338 value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1],
339 plaintext[current_position + 2], plaintext[current_position + 3]);
340 current_position += value_size;
341 break;
342 }
344 value_size = 2;
345 if (current_position + value_size > message_length) {
346 ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED");
347 this->receive_buffer_.clear();
348 return;
349 }
350 value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
351 current_position += value_size;
352 break;
353 }
355 uint8_t data_length = plaintext[current_position];
356 current_position++; // Advance past string length
357 if (current_position + data_length > message_length) {
358 ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING");
359 this->receive_buffer_.clear();
360 return;
361 }
362 // Handle timestamp (normal OBIS code or NETZNOE special case)
363 if (obis_cd == OBIS_TIMESTAMP || timestamp_found) {
364 if (data_length < 8) {
365 ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length);
366 this->receive_buffer_.clear();
367 return;
368 }
369 uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
370 uint8_t month = plaintext[current_position + 2];
371 uint8_t day = plaintext[current_position + 3];
372 uint8_t hour = plaintext[current_position + 5];
373 uint8_t minute = plaintext[current_position + 6];
374 uint8_t second = plaintext[current_position + 7];
375 if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) {
376 ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute,
377 second);
378 this->receive_buffer_.clear();
379 return;
380 }
381 snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour,
382 minute, second);
383 } else if (meter_number_found) {
384 snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]);
385 }
386 current_position += data_length;
387 break;
388 }
389 default:
390 ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type);
391 this->receive_buffer_.clear();
392 return;
393 }
394
395 // Skip break after data
396 if (this->provider_ == PROVIDER_NETZNOE) {
397 // Don't skip the break on the first timestamp, as there's none
398 if (!timestamp_found) {
399 current_position += 2;
400 }
401 } else {
402 current_position += 2;
403 }
404
405 // Check for additional data (scaler-unit structure)
406 if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) {
407 // Apply scaler: real_value = raw_value × 10^scaler
408 if (current_position + 1 < message_length) {
409 int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
410 if (scaler != 0) {
411 value *= pow10_int(scaler);
412 }
413 }
414
415 // on EVN Meters there is no additional break
416 if (this->provider_ == PROVIDER_NETZNOE) {
417 current_position += 4;
418 } else {
419 current_position += 6;
420 }
421 }
422
423 // Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED)
424 if (value_size > 0) {
425 switch (obis_cd) {
426 case OBIS_VOLTAGE_L1:
427 data.voltage_l1 = value;
428 break;
429 case OBIS_VOLTAGE_L2:
430 data.voltage_l2 = value;
431 break;
432 case OBIS_VOLTAGE_L3:
433 data.voltage_l3 = value;
434 break;
435 case OBIS_CURRENT_L1:
436 data.current_l1 = value;
437 break;
438 case OBIS_CURRENT_L2:
439 data.current_l2 = value;
440 break;
441 case OBIS_CURRENT_L3:
442 data.current_l3 = value;
443 break;
444 case OBIS_ACTIVE_POWER_PLUS:
445 data.active_power_plus = value;
446 break;
447 case OBIS_ACTIVE_POWER_MINUS:
448 data.active_power_minus = value;
449 break;
450 case OBIS_ACTIVE_ENERGY_PLUS:
451 data.active_energy_plus = value;
452 break;
453 case OBIS_ACTIVE_ENERGY_MINUS:
454 data.active_energy_minus = value;
455 break;
456 case OBIS_REACTIVE_ENERGY_PLUS:
457 data.reactive_energy_plus = value;
458 break;
459 case OBIS_REACTIVE_ENERGY_MINUS:
460 data.reactive_energy_minus = value;
461 break;
462 case OBIS_POWER_FACTOR:
463 data.power_factor = value;
464 power_factor_found = true;
465 break;
466 default:
467 ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd);
468 }
469 }
470 }
471
472 this->receive_buffer_.clear();
473
474 ESP_LOGI(TAG, "Received valid data");
475 this->publish_sensors(data);
476 this->status_clear_warning();
477}
478
479} // namespace esphome::dlms_meter
uint8_t checksum
Definition bl0906.h:3
void status_clear_warning()
Definition component.h:254
bool parse_dlms_(const std::vector< uint8_t > &mbus_payload, uint16_t &message_length, uint8_t &systitle_length, uint16_t &header_offset)
bool decrypt_(std::vector< uint8_t > &mbus_payload, uint16_t message_length, uint8_t systitle_length, uint16_t header_offset)
std::vector< uint8_t > mbus_payload_
Definition dlms_meter.h:88
bool parse_mbus_(std::vector< uint8_t > &mbus_payload)
void decode_obis_(uint8_t *plaintext, uint16_t message_length)
void publish_sensors(MeterData &data)
Definition dlms_meter.h:64
std::vector< uint8_t > receive_buffer_
Definition dlms_meter.h:87
std::array< uint8_t, 16 > decryption_key_
Definition dlms_meter.h:93
DLMS_METER_SENSOR_LIST(SUB_SENSOR,) DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
uint8_t month
Definition date_entity.h:1
uint16_t year
Definition date_entity.h:0
uint8_t day
Definition date_entity.h:2
uint8_t second
uint8_t minute
uint8_t hour
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:736
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:728
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
float pow10_int(int8_t exp)
Compute 10^exp using iterative multiplication/division.
Definition helpers.h:592