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