MBusinoLibPy 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1148 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Core class ``MBusinoLib`` - a 1:1 port of ``MBusinoLib.cpp`` (C++/Arduino).
3
+
4
+ Every method states in its comment the corresponding C++ function plus the
5
+ line number in ``MBusinoLib/src/MBusinoLib.cpp``, so you can switch
6
+ between the two implementations function by function.
7
+
8
+ Deliberate deviations from the original (spec item 4 of the porting task):
9
+
10
+ * The internal buffer plus ``reset()``/``getBuffer()``/``copy()``/``getSize()``
11
+ is dropped completely - the Python API takes ``bytes``/``bytearray`` as
12
+ arguments. Only ``_error`` remains, just like the C++ ``_error``.
13
+ * ``get_error()`` behaves like the C++ version: it returns **and** clears
14
+ the error (MBusinoLib.cpp line 54-58).
15
+ * Instead of UB (out-of-bounds reads in the original) Python stops cleanly
16
+ and sets ``BUFFER_OVERFLOW`` - every affected spot is commented
17
+ individually.
18
+
19
+ All numeric operations run in the same order and with the same types as in
20
+ the C++ code (``int16_t``/``int32_t`` wrap-around, truncation of the
21
+ ``int8_t`` scalar, 32-bit ``float`` for "real" records), so the values
22
+ are bit-identical. Only ArduinoJson's *output* rounds, therefore the
23
+ golden tests compare ``value_scaled`` with a tolerance
24
+ (relative 1e-6 / absolute 1e-6, see ``ARDUINO_FLOAT_DECIMALS = 6``).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import struct
30
+ from enum import IntEnum # noqa: F401 (imported only so the type is available)
31
+
32
+ from .enums import (
33
+ ARDUINO_FLOAT_DECIMALS, # noqa: F401 (Dokumentationswert, s. enums.py)
34
+ ARDUINO_FLOAT_MIN, # noqa: F401
35
+ MBUS_DEFAULT_BUFFER_SIZE, # noqa: F401
36
+ MBUS_CODE,
37
+ MBUS_ERROR,
38
+ )
39
+ from .lookup_tables import (
40
+ CODE_NAME,
41
+ CODE_UNITS,
42
+ DEVICE_CLASS,
43
+ MEDIUM_NAMES,
44
+ STATE_CLASS,
45
+ _volume_class_from_medium,
46
+ )
47
+ from .vif_defs import VIF_DEFS
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # NEW (not present in MBusinoLib): readable error messages
51
+ # ---------------------------------------------------------------------------
52
+ # Mapping follows the `enum MBUS_ERROR` from MBusinoLib.h line 185-192.
53
+ # UNSUPPORTED_CODING / UNSUPPORTED_RANGE / NEGATIVE_VALUE are leftovers
54
+ # from the old AllWize encoder - the decoder never sets them
55
+ # (proof: the only error assignments in MBusinoLib.cpp are lines 226,
56
+ # 283 and 382), but they are listed here for completeness.
57
+ _ERROR_MESSAGES = {
58
+ MBUS_ERROR.NO_ERROR: "No error.",
59
+ MBUS_ERROR.BUFFER_OVERFLOW: (
60
+ "Buffer too small or telegram incomplete: the declared length does not "
61
+ "fit into the given data, or a record would run past the end of the "
62
+ "buffer."
63
+ ),
64
+ MBUS_ERROR.UNSUPPORTED_CODING: (
65
+ "Unsupported data coding (DIF coding)."
66
+ ),
67
+ MBUS_ERROR.UNSUPPORTED_RANGE: (
68
+ "Unsupported value range: combination of code and scaling not "
69
+ "representable."
70
+ ),
71
+ MBUS_ERROR.UNSUPPORTED_VIF: (
72
+ "Unknown or unsupported VIF - the record could not be matched to any "
73
+ "definition in vif_defs."
74
+ ),
75
+ MBUS_ERROR.NEGATIVE_VALUE: (
76
+ "Negative value, which is not supported by the original."
77
+ ),
78
+ }
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Small helpers for the C++ type semantics
83
+ # ---------------------------------------------------------------------------
84
+ class _OutOfBounds(Exception):
85
+ """Internal: read outside the real buffer (in C++ this would be UB)."""
86
+
87
+
88
+ def _to_int8(value: int) -> int:
89
+ """C++: assignment of an expression (uint32_t arithmetic) to ``int8_t``.
90
+
91
+ GCC/Clang truncate to the low 8 bits and read them as two's complement
92
+ (defined in C++20, implementation-defined before that).
93
+ ``2**32`` is a multiple of 256, so truncating to 8 bits is enough -
94
+ also for negative Python intermediate values (``-3 & 0xFF == 0xFD``).
95
+ """
96
+ low = value & 0xFF
97
+ return low - 0x100 if low & 0x80 else low
98
+
99
+
100
+ def _to_int16(value: int) -> int:
101
+ """C++: ``value = (int64_t)value16;`` with ``int16_t value16`` (sign extension)."""
102
+ low = value & 0xFFFF
103
+ return low - 0x10000 if low & 0x8000 else low
104
+
105
+
106
+ def _to_int32(value: int) -> int:
107
+ """C++: ``value = (int64_t)value32;`` with ``int32_t value32`` (sign extension)."""
108
+ low = value & 0xFFFFFFFF
109
+ return low - 0x100000000 if low & 0x80000000 else low
110
+
111
+
112
+ def _parse_hex(text: str):
113
+ """Corresponds to ``parseHex()`` in test_decode.cpp line 10-22.
114
+
115
+ Splits "68 56 56 68 ..." / "0x68,0x56,..." input into a byte sequence.
116
+ Convenience for ``decode_telegram("68 56 ...")`` - the original reads
117
+ the input only from argv/stdin, here a plain string is accepted as well.
118
+ """
119
+ raw = []
120
+ for token in text.replace(",", " ").split():
121
+ token = token.strip()
122
+ if token.lower().startswith("0x"):
123
+ token = token[2:]
124
+ if not token:
125
+ continue
126
+ raw.append(int(token, 16) & 0xFF)
127
+ return bytes(raw)
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # The class
132
+ # ---------------------------------------------------------------------------
133
+ class MBusinoLib:
134
+ """Decoder for M-Bus RSP_UD telegrams - counterpart of the C++ class.
135
+
136
+ C++: ``class MBusinoLib`` in MBusinoLib.h line 369-399.
137
+
138
+ Example::
139
+
140
+ decode = MBusinoLib()
141
+ records = decode.decode_records(telegram[0x13:packet_size],
142
+ packet_size - 0x13 - 2)
143
+ print(decode.get_error())
144
+ """
145
+
146
+ # __init__ corresponds to MBusinoLib::MBusinoLib() in MBusinoLib.cpp line 27-31.
147
+ # That constructor only allocates the work buffer and sets _cursor/_error;
148
+ # there is no buffer in Python (spec item 4) and _error starts at
149
+ # NO_ERROR like in C++ (MBusinoLib.h line 398: ``uint8_t _error = NO_ERROR;``).
150
+ def __init__(self) -> None:
151
+ self._error: IntEnum = MBUS_ERROR.NO_ERROR
152
+
153
+ # ------------------------------------------------------------------
154
+ # Errors
155
+ # ------------------------------------------------------------------
156
+ def get_error(self) -> int:
157
+ """Corresponds to MBusinoLib::getError() in MBusinoLib.cpp line 54-58.
158
+
159
+ Like the original: returns the error code AND clears the internal
160
+ error back to ``NO_ERROR``. To keep the value, read
161
+ ``decode._error`` or call ``get_error_message()`` FIRST.
162
+ """
163
+ error = self._error
164
+ self._error = MBUS_ERROR.NO_ERROR
165
+ return int(error)
166
+
167
+ def get_error_message(self, code: int = None) -> str:
168
+ """NEW (not in C++): readable English error message.
169
+
170
+ Without an argument the method reports the currently stored error -
171
+ unlike ``get_error()`` it clears nothing, so you can read
172
+ ``get_error_message()`` first and ``get_error()`` afterwards.
173
+
174
+ :param code: optional MBUS_ERROR value instead of the internal state
175
+
176
+ Example::
177
+
178
+ >>> MBusinoLib().get_error_message()
179
+ 'No error.'
180
+ >>> MBusinoLib().get_error_message(4) # MBUS_ERROR.UNSUPPORTED_VIF
181
+ 'Unknown or unsupported VIF - the record could not be matched to any definition in vif_defs.'
182
+ """
183
+ if code is None:
184
+ code = self._error
185
+ try:
186
+ return _ERROR_MESSAGES.get(MBUS_ERROR(code), "Unknown error code %s." % code)
187
+ except ValueError:
188
+ # Value outside the enum (e.g. a raw/error value from somewhere else)
189
+ return "Unknown error code %s." % code
190
+
191
+ # ------------------------------------------------------------------
192
+ # Lookup tables (correspond to the four C++ switch methods)
193
+ # ------------------------------------------------------------------
194
+ def get_code_name(self, code: int) -> str:
195
+ """Corresponds to MBusinoLib::getCodeName() in MBusinoLib.cpp line 847-1165.
196
+
197
+ :param code: MBUS_CODE value (e.g. ``MBUS_CODE.ENERGY_WH``)
198
+ :return: record name like "energy", "" for unknown codes (C++: default)
199
+ """
200
+ return CODE_NAME.get(code, "")
201
+
202
+ def get_code_units(self, code: int):
203
+ """Corresponds to MBusinoLib::getCodeUnits() in MBusinoLib.cpp line 660-845.
204
+
205
+ :return: unit ("Wh", "m³", ...) or ``None`` - ``None`` corresponds to
206
+ the NULL pointer the original returns via ``return 0;``
207
+
208
+ Example::
209
+
210
+ >>> MBusinoLib().get_code_units(MBUS_CODE.VOLUME_M3)
211
+ 'm³'
212
+ >>> MBusinoLib().get_code_units(MBUS_CODE.UNKNOWN_VIF) is None # C++: return 0 / NULL
213
+ True
214
+ """
215
+ return CODE_UNITS.get(code) # no entry -> None, just like in C++
216
+
217
+ def get_device_class(self, code: int, medium: int = 0xFF) -> str:
218
+ """Corresponds to MBusinoLib::getDeviceClass() in MBusinoLib.cpp line 1182-1380.
219
+
220
+ :param code: MBUS_CODE value
221
+ :param medium: M-Bus medium byte from ``decode_header()`` ("medium_code"),
222
+ default 0xFF = unknown - like the C++ default parameter
223
+ :return: Home Assistant device_class, "" when none applies
224
+
225
+ Example (the medium byte selects water/gas for volume records)::
226
+
227
+ >>> MBusinoLib().get_device_class(MBUS_CODE.VOLUME_M3, medium=0x07)
228
+ 'water'
229
+ >>> MBusinoLib().get_device_class(MBUS_CODE.VOLUME_M3, medium=0x03)
230
+ 'gas'
231
+ """
232
+ # C++ line 1202-1206: the three volume codes call the medium helper
233
+ # (case VOLUME_M3 / VOLUME_FT3 / VOLUME_GAL).
234
+ if code in (MBUS_CODE.VOLUME_M3, MBUS_CODE.VOLUME_FT3, MBUS_CODE.VOLUME_GAL):
235
+ return _volume_class_from_medium(medium)
236
+ return DEVICE_CLASS.get(code, "")
237
+
238
+ def get_state_class(self, code: int) -> str:
239
+ """Corresponds to MBusinoLib::getStateClass() in MBusinoLib.cpp line 1382-1525.
240
+
241
+ :return: "total_increasing" / "measurement" / "" (C++: default)
242
+ """
243
+ # SPECIAL_SUPPLIER_INFO has no own case in the C++ switch and falls
244
+ # through default to "" - .get() does the same here.
245
+ return STATE_CLASS.get(code, "")
246
+
247
+ # ------------------------------------------------------------------
248
+ # Internal helpers (correspond to the protected C++ methods)
249
+ # ------------------------------------------------------------------
250
+ @staticmethod
251
+ def _find_definition(vif: int) -> int:
252
+ """Corresponds to MBusinoLib::_findDefinition() in MBusinoLib.cpp line 1528-1539.
253
+
254
+ Searches for the FIRST table entry with ``base <= vif < base + size``.
255
+
256
+ :return: index into ``VIF_DEFS`` or -1 when nothing was found
257
+ (C++: ``int16_t``, returns -1)
258
+ """
259
+ for index, (_code, base, size, _scalar) in enumerate(VIF_DEFS):
260
+ if base <= vif < (base + size):
261
+ return index
262
+ return -1
263
+
264
+ @staticmethod
265
+ def _get_vif(code: int, scalar: int) -> int:
266
+ """Corresponds to MBusinoLib::_getVIF() in MBusinoLib.cpp line 1541-1555.
267
+
268
+ Inverse of ``_find_definition``: returns the VIF matching code and
269
+ scaling (the original never calls it while decoding, only the
270
+ AUnit tests do).
271
+
272
+ :return: VIF value or 0xFF when no valid combination exists
273
+ (C++ comment: "this is not a valid VIF")
274
+ """
275
+ for _code, base, size, def_scalar in VIF_DEFS:
276
+ if code == _code:
277
+ if def_scalar <= scalar < (def_scalar + size):
278
+ return base + (scalar - def_scalar)
279
+ return 0xFF
280
+
281
+ # ------------------------------------------------------------------
282
+ # Records
283
+ # ------------------------------------------------------------------
284
+ def decode_records(self, buffer, size: int = None) -> list:
285
+ """Corresponds to MBusinoLib::decodeRecords() in MBusinoLib.cpp line 62-657.
286
+
287
+ Splits the payload area of a telegram into individual records.
288
+
289
+ :param buffer: bytes starting at the first DIF (offset 0x13 for telegrams)
290
+ :param size: number of bytes to read - exactly like the C++ parameter
291
+ ``uint8_t size``. ``test_decode.cpp`` passes
292
+ ``packet_size - 0x13 - 2`` (i.e. WITHOUT checksum and
293
+ stop byte even though ``buffer`` is 2 bytes longer).
294
+ ``None`` (default) means ``len(buffer)``.
295
+ :return: list of records - identical to the C++ ``JsonArray``, every
296
+ entry has the keys
297
+ ``vif``, ``code``, ``value_scaled``, ``value_string``,
298
+ ``units``, ``name`` and optionally ``subUnit``, ``storage``,
299
+ ``tariff``, ``telegramFollow``.
300
+ NOTE: on error the C++ return value would be 0, but the
301
+ JsonArray already holds records decoded so far - Python
302
+ returns those (the error is reported by ``get_error()``).
303
+ """
304
+ data = bytes(buffer)
305
+ buf_len = len(data)
306
+ if size is None:
307
+ size = buf_len
308
+
309
+ records: list = []
310
+ index = 0
311
+
312
+ def rd(pos: int) -> int:
313
+ """Read one byte like ``buffer[pos]`` in C++ - there WITHOUT a check.
314
+
315
+ Every access that would run past the end in the original (UB)
316
+ goes through this function and raises
317
+ ``_OutOfBounds`` -> ``BUFFER_OVERFLOW``.
318
+ """
319
+ if 0 <= pos < buf_len:
320
+ return data[pos]
321
+ raise _OutOfBounds(pos)
322
+
323
+ try:
324
+ # `while (index < size)` - MBusinoLib.cpp line 66
325
+ while index < size:
326
+ # ------------------------------------------------------------ #
327
+ # decode the DIF - MBusinoLib.cpp line 71-162
328
+ # ------------------------------------------------------------ #
329
+ dif = rd(index) # C++: uint8_t dif = buffer[index++];
330
+ index += 1
331
+ dif_least4bit = dif & 0x0F
332
+ dif_function_field = (dif & 0x30) >> 4
333
+
334
+ # switch(difLeast4bit) - MBusinoLib.cpp line 98-162:
335
+ # -> (length in bytes, data coding type)
336
+ # 0 = no data, 1 = integer, 2 = BCD, 3 = real (float32),
337
+ # 4 = variable length, 5 = special function,
338
+ # 6 = date+time type F, 7 = date type G
339
+ # (6/7 are applied further down via the VIF)
340
+ #
341
+ # C++ table (line 80-95): DIF bits 0..3 = length/coding,
342
+ # bits 4..5 = function (0=current,1=_max,2=_min,3=_err)
343
+ length, data_coding_type = _DIF_TABLE[dif_least4bit]
344
+
345
+ # append the function field - MBusinoLib.cpp line 164-179
346
+ function_field = _FUNCTION_FIELDS[dif_function_field]
347
+
348
+ # ------------------------------------------------------------ #
349
+ # handle the DIFEs - MBusinoLib.cpp line 181-211
350
+ # ------------------------------------------------------------ #
351
+ # bit 6 of the DIF pre-sets "storage 1"
352
+ storage_number = 1 if (dif & 0x40) == 0x40 else 0
353
+ dife_number = 0
354
+ # C++: uint8_t dife[10] = {0}; (index 0 stays unused,
355
+ # the DIFEs start at index 1 - therefore 11 slots here, so
356
+ # the C++ access dife[difeNumber+1] (line 197-208) at the end
357
+ # of the loop hits the value 0 instead of running past the array)
358
+ dife = [0] * 11
359
+ if_dife = (dif & 0x80) == 0x80 # Bit 7 = "eine DIFE folgt"
360
+ while if_dife:
361
+ dife_number += 1
362
+ if dife_number > 9:
363
+ # C++ writes past dife[10] here (UB)
364
+ raise _OutOfBounds(dife_number)
365
+ dife[dife_number] = rd(index) # C++ line 191
366
+ if_dife = (rd(index) & 0x80) == 0x80 # weitere DIFE?
367
+ index += 1
368
+
369
+ # evaluate the DIFEs - MBusinoLib.cpp line 194-210
370
+ sub_unit = 0
371
+ tariff = 0
372
+ i = 0
373
+ # C++: for(uint8_t i=0; difeNumber>0 && i<=difeNumber; i++)
374
+ while dife_number > 0 and i <= dife_number:
375
+ if i == 0:
376
+ storage_number |= (dife[i + 1] & 0x0F) << 1
377
+ else:
378
+ storage_number |= (dife[i + 1] & 0x0F) << (4 * i)
379
+ sub_unit |= (((dife[i + 1] & 0x40) >> 6) << i)
380
+ tariff |= (((dife[i + 1] & 0x30) >> 4) << (2 * i))
381
+ i += 1
382
+
383
+ # end of DIFE handling (C++: "// End of DIFE handling" line 212)
384
+
385
+ # ------------------------------------------------------------ #
386
+ # fetch the VIF(E) - MBusinoLib.cpp line 213-246
387
+ # ------------------------------------------------------------ #
388
+ vif = 0
389
+ vifarray = [0] * 10 # C++: uint8_t vifarray[10] = {0};
390
+ vifcounter = 0
391
+ custom_vif_len = 0 # C++: uint8_t customVIFlen = 0;
392
+ custom_vif = "" # C++: char customVIF[20] = {0};
393
+ if_custom_vif = False # C++: bool ifcustomVIF = false;
394
+
395
+ while True:
396
+ if index == size:
397
+ # C++ line 225-228: explicit BUFFER_OVERFLOW
398
+ self._error = MBUS_ERROR.BUFFER_OVERFLOW
399
+ return records
400
+ if vifcounter > 9:
401
+ # C++ reads past vifarray[10] here (UB)
402
+ raise _OutOfBounds(vifcounter)
403
+ vifarray[vifcounter] = rd(index) # C++ line 230
404
+ index += 1
405
+
406
+ if (vifarray[0] & 0x7F) == 0x7C and vifcounter == 0:
407
+ # Customized ASCII VIF (C++ line 232-237):
408
+ # the next byte is the length of the ASCII string
409
+ custom_vif_len = rd(index)
410
+ if vifarray[0] == 0xFC:
411
+ # for a 0xFC VIF skip the ASCII string in order
412
+ # to reach the VIFE(s) behind it
413
+ index = index + custom_vif_len + 1
414
+
415
+ if vifcounter < 2:
416
+ # normally VIF + first VIFE are enough (C++ line 239)
417
+ vif = (vif << 8) + vifarray[vifcounter]
418
+ elif vifcounter == 2 and vif == 0xFDFD:
419
+ # ... unless VIF 0xFDFD needs the third byte (line 242)
420
+ vif = (vif << 8) + vifarray[vifcounter]
421
+
422
+ vifcounter += 1
423
+ # stop as soon as bit 7 of the last byte read is missing
424
+ if (vifarray[vifcounter - 1] & 0x80) != 0x80:
425
+ break
426
+
427
+ # ------------------------------------------------------------ #
428
+ # fine-tune the VIF - MBusinoLib.cpp line 248-276
429
+ # ------------------------------------------------------------ #
430
+ # When the top bit is set in the FIRST byte and it is none of
431
+ # the "extension marker bytes" 0xFD/0xFC/0xFB/0xFF, the real
432
+ # VIF sits in this byte alone (mask the extension bit away).
433
+ if (
434
+ (vifarray[0] & 0x80) == 0x80
435
+ and vifarray[0] != 0xFD
436
+ and vifarray[0] != 0xFC
437
+ and vifarray[0] != 0xFB
438
+ and vifarray[0] != 0xFF
439
+ ):
440
+ vif = vifarray[0] & 0x7F
441
+
442
+ if vifarray[0] == 0x7C:
443
+ vif = 0x7C00 # C++ line 253-255
444
+
445
+ # 0x6D = date+time type F, 0x6C = date type G -> overwrite
446
+ # the coding independently of the DIF (C++ line 257-263)
447
+ if (vif & 0x7F) == 0x6D:
448
+ data_coding_type = 6
449
+ elif (vif & 0x7F) == 0x6C:
450
+ data_coding_type = 7
451
+ elif (vif & 0x7F00) == 0x7C00:
452
+ # Customized ASCII VIF: actually read the string now
453
+ # (C++ line 264-277)
454
+ vif = 0xFC00
455
+ if vifarray[0] == 0xFC:
456
+ # for 0xFC the string was already skipped above,
457
+ # therefore rewind the pointer
458
+ index = index - custom_vif_len - 1
459
+ # C++: char vifBuffer[customVIFlen + 1]; (uninitialised!)
460
+ vif_buffer = [0] * (custom_vif_len + 1)
461
+ for i in range(custom_vif_len + 1):
462
+ # C++ line 271: vifBuffer[customVIFlen-i] = buffer[index - vifcounter + 1];
463
+ # The read start shifts by -vifcounter because index
464
+ # is further ahead in the 0xFC case (past the VIFE(s)) -
465
+ # at the end the pointer is exactly on the length byte.
466
+ vif_buffer[custom_vif_len - i] = rd(index - vifcounter + 1)
467
+ index += 1
468
+ # C++: strncpy(customVIF, vifBuffer, customVIFlen);
469
+ # vifBuffer[customVIFlen] holds the length byte and
470
+ # stays unused; the characters are stored reversed
471
+ # (M-Bus sends ASCII backwards) and are flipped back into
472
+ # reading order here.
473
+ # char customVIF[20] -> at most 19 characters + NUL.
474
+ # (C++ would run past the buffer for longer strings)
475
+ custom_vif = bytes(vif_buffer[:custom_vif_len]).decode(
476
+ "latin-1", errors="replace"
477
+ )[:19]
478
+ if_custom_vif = True
479
+
480
+ # clear bit 7 of the last VIF byte (C++ line 278), otherwise
481
+ # _findDefinition() finds no match
482
+ vif = vif & 0xFFFFFF7F
483
+
484
+ # ------------------------------------------------------------ #
485
+ # look up the definition - MBusinoLib.cpp line 280-287
486
+ # ------------------------------------------------------------ #
487
+ def_index = self._find_definition(vif)
488
+ if def_index < 0:
489
+ # C++ sets the error and then CONTINUES with entry 0
490
+ # (UNKNOWN_VIF) - the "return 0" line is commented out
491
+ # there. Python behaves exactly the same way.
492
+ self._error = MBUS_ERROR.UNSUPPORTED_VIF
493
+ def_index = 0
494
+
495
+ # ------------------------------------------------------------ #
496
+ # VIFE extensions - MBusinoLib.cpp line 288-378
497
+ # ------------------------------------------------------------ #
498
+ extension_scaler = 0 # C++: int8_t extensionScaler
499
+ extension_additive_constant = 0.0 # C++: double extensionAdditiveConstant
500
+ name_extension = "" # C++: char stringNameExtension[10]
501
+ no_unit = False # C++: bool noUnit
502
+
503
+ if vifcounter - 1 > 0: # at least one VIFE present
504
+ if (
505
+ vifarray[0] == 0xFB
506
+ or (vifarray[0] == 0xFD and vifarray[1] != 0xFD)
507
+ or vifarray[0] == 0xFF
508
+ ):
509
+ first_vife_extension = 2
510
+ elif vifarray[0] == 0xFD and vifarray[1] == 0xFD:
511
+ first_vife_extension = 3
512
+ else:
513
+ first_vife_extension = 1
514
+
515
+ extensions_counter = first_vife_extension
516
+ # C++: do { ... } while(extensionsCounter <= vifcounter);
517
+ while True:
518
+ if extensions_counter > 9:
519
+ # C++ reads past vifarray[10] here (UB)
520
+ raise _OutOfBounds(extensions_counter)
521
+ byte = vifarray[extensions_counter]
522
+
523
+ if (byte & 0x7F) == 0x7D:
524
+ # E111 1101: multiplicative correction factor 1000
525
+ extension_scaler = 3
526
+ elif (byte & 0x78) == 0x70:
527
+ # E111 0nnn: multiplicative factor 10^(nnn-6)
528
+ extension_scaler = (byte & 0x07) - 6
529
+ elif (byte & 0x7C) == 0x78:
530
+ # E111 10nn: additive correction constant 10^(nn-3)
531
+ additive_scaler = (byte & 0x03) - 3
532
+ extension_additive_constant = 1.0
533
+ for _ in range(additive_scaler):
534
+ extension_additive_constant *= 10.0
535
+ for _ in range(-additive_scaler if additive_scaler < 0 else 0):
536
+ extension_additive_constant /= 10.0
537
+ elif (byte & 0x7A) == 0x6A:
538
+ # E110 1f1b: date/time - adapt the coding
539
+ if dif_least4bit == 4:
540
+ data_coding_type = 6
541
+ elif dif_least4bit == 2:
542
+ data_coding_type = 7
543
+ name_extension = "_TimeSt"
544
+ no_unit = True
545
+ elif byte == 0xFC or byte == 0xFF:
546
+ # "reading user" extension: the NEXT byte
547
+ # selects phase/reference (C++ line 334-372)
548
+ if extensions_counter + 1 > 9:
549
+ raise _OutOfBounds(extensions_counter + 1)
550
+ extension_buffer = vifarray[extensions_counter + 1] & 0x7F
551
+ suffix = _PHASE_SUFFIX.get(extension_buffer)
552
+ if suffix is not None:
553
+ name_extension = suffix
554
+ extensions_counter += 1 # C++ skips over that byte
555
+ extensions_counter += 1
556
+ if extensions_counter > vifcounter:
557
+ break
558
+
559
+ # ------------------------------------------------------------ #
560
+ # buffer overflow check - MBusinoLib.cpp line 380-384
561
+ # ------------------------------------------------------------ #
562
+ if index + length > size or index + length > buf_len:
563
+ self._error = MBUS_ERROR.BUFFER_OVERFLOW
564
+ return records
565
+
566
+ # ------------------------------------------------------------ #
567
+ # read the value - MBusinoLib.cpp line 386-580
568
+ # ------------------------------------------------------------ #
569
+ value16 = 0 # C++ line 387: int16_t - notices negative 2-byte values
570
+ value32 = 0 # C++ line 388: int32_t - notices negative 4-byte values
571
+ value = 0 # C++ line 389: int64_t
572
+ value_float = 0.0 # C++ line 391: float (a real 32-bit number!)
573
+ date = b"" # C++ line 395: uint8_t date[len]
574
+ datestring = "" # C++ line 396: char datestring[16]
575
+ datestring2 = "" # C++ line 397: char datestring2[30]
576
+ value_string = "" # C++ line 398: char valueString[30]
577
+ switch_again = False # C++ line 399
578
+ negative = False # C++ line 400
579
+ ascii_value = 0 # C++ line 401: 0=double, 1=ASCII, 2=both
580
+
581
+ # C++: do { switch(dataCodingType) ... } while(switchAgain);
582
+ while True:
583
+ switch_again = False
584
+
585
+ if data_coding_type == 0:
586
+ # case 0 (C++ line 405): no data, value stays 0
587
+ pass
588
+
589
+ elif data_coding_type == 1:
590
+ # case 1 (C++ line 408-425): integer, LITTLE-ENDIAN
591
+ # (the loop builds the byte at index len-i-1 into the
592
+ # most significant byte - M-Bus sends LSB first)
593
+ if length == 2:
594
+ value16 = 0
595
+ for i in range(length):
596
+ value16 = (value16 << 8) + rd(index + length - i - 1)
597
+ value = _to_int16(value16) # C++: (int64_t)value16
598
+ elif length == 4:
599
+ value32 = 0
600
+ for i in range(length):
601
+ value32 = (value32 << 8) + rd(index + length - i - 1)
602
+ value = _to_int32(value32) # C++: (int64_t)value32
603
+ else:
604
+ value = 0
605
+ for i in range(length):
606
+ value = (value << 8) + rd(index + length - i - 1)
607
+
608
+ elif data_coding_type == 2:
609
+ # case 2 (C++ line 427-461): BCD, 2 digits per byte
610
+ if length == 2:
611
+ value16 = 0
612
+ elif length == 4:
613
+ value32 = 0
614
+ else:
615
+ value = 0
616
+ for i in range(length):
617
+ byte = rd(index + length - i - 1)
618
+ if i == 0 and (byte & 0xF0) == 0xF0:
619
+ # Only unsigned BCD is intended, but some
620
+ # manufacturers put an F into the top half
621
+ # byte as a "negative" marker.
622
+ # QUIRK of the original: the tens digit of
623
+ # that byte is lost as well
624
+ # (the byte is reduced to 0x0F).
625
+ byte = byte & 0x0F
626
+ negative = True
627
+ if length == 2:
628
+ value16 = value16 * 100 + ((byte >> 4) * 10) + (byte & 0x0F)
629
+ elif length == 4:
630
+ value32 = value32 * 100 + ((byte >> 4) * 10) + (byte & 0x0F)
631
+ else:
632
+ value = value * 100 + ((byte >> 4) * 10) + (byte & 0x0F)
633
+ if length == 2:
634
+ value = _to_int16(value16)
635
+ elif length == 4:
636
+ value = _to_int32(value32)
637
+ if negative:
638
+ # C++ line 458-460: only BCD has the negative case
639
+ value = value * -1
640
+
641
+ elif data_coding_type == 3:
642
+ # case 3 (C++ line 464-467): real = IEEE-754 32 bit
643
+ value = 0
644
+ for i in range(length):
645
+ value = (value << 8) + rd(index + length - i - 1)
646
+ # C++: memcpy(&valueFloat, &value, sizeof(valueFloat))
647
+ # -> the LOW 4 bytes of value are read as float
648
+ # (same byte order as above).
649
+ value_float = struct.unpack(
650
+ "<f", struct.pack("<I", value & 0xFFFFFFFF)
651
+ )[0]
652
+
653
+ elif data_coding_type == 4:
654
+ # case 4 (C++ line 470-522): variable length, LVAR
655
+ # 00..BFh ASCII, C0..CFh positive BCD,
656
+ # D0..DFh negative BCD, E0..EFh bytes as integer,
657
+ # F0..FAh bytes as float, FB..FFh reserved
658
+ lvar = rd(index)
659
+ if lvar <= 0xBF:
660
+ # ASCII string with LVAR characters (C++ line 483-494)
661
+ length = lvar
662
+ index += 1
663
+ char_buffer = [0] * (length + 1)
664
+ for i in range(length + 1):
665
+ # characters arrive reversed like in the original:
666
+ # this restores reading order
667
+ char_buffer[i] = rd(index + (length - i - 1))
668
+ # C++: strncpy(valueString, charBuffer, len);
669
+ # char valueString[30] -> at most 29 characters
670
+ value_string = bytes(char_buffer[:length]).decode(
671
+ "latin-1", errors="replace"
672
+ )[:29]
673
+ ascii_value = 1
674
+ elif 0xC0 <= lvar <= 0xCF:
675
+ # positive BCD, (LVAR - C0h) bytes (C++ line 495)
676
+ length = lvar - 0xC0
677
+ index += 1
678
+ data_coding_type = 2
679
+ switch_again = True
680
+ elif 0xD0 <= lvar <= 0xDF:
681
+ # negative BCD (C++ line 501)
682
+ length = lvar - 0xD0
683
+ index += 1
684
+ data_coding_type = 2
685
+ switch_again = True
686
+ negative = True
687
+ elif 0xE0 <= lvar <= 0xEF:
688
+ # binary number, (LVAR - E0h) bytes (C++ line 508)
689
+ length = lvar - 0xE0
690
+ index += 1
691
+ data_coding_type = 1
692
+ switch_again = True
693
+ elif 0xF0 <= lvar <= 0xFA:
694
+ # floating point, (LVAR - F0h) bytes (C++ line 515)
695
+ length = lvar - 0xF0
696
+ index += 1
697
+ data_coding_type = 3
698
+ switch_again = True
699
+ # FB..FF reserved -> nothing happens, as in the original,
700
+ # length stays 0 and the next record follows
701
+
702
+ elif data_coding_type == 5:
703
+ # case 5 (C++ line 524): special function, nothing to do
704
+ pass
705
+
706
+ elif data_coding_type == 6:
707
+ # case 6 (C++ line 527-552): date+time type F, 4 bytes
708
+ date = data[index:index + length]
709
+ if len(date) < 4:
710
+ # C++ reads date[0..3] out of a too small VLA (UB)
711
+ raise _OutOfBounds(len(date))
712
+ if (date[0] & 0x80) == 0:
713
+ # bit 7 of the minutes byte = "time invalid" (C++ line 531)
714
+ year = ((date[2] & 0xE0) >> 5) | ((date[3] & 0xF0) >> 1)
715
+ month = date[3] & 0x0F
716
+ mday = date[2] & 0x1F
717
+ hour = date[1] & 0x1F
718
+ minute = date[0] & 0x3F
719
+ # C++ snprintf("20%02d%02d%02d%02d%02d") - the
720
+ # formats produce exactly the same strings.
721
+ # (The buffer length of 24 given there for
722
+ # char[16] is a BUG, but it is never reached,
723
+ # because at most 13 characters are produced.)
724
+ datestring = "20%02d%02d%02d%02d%02d" % (
725
+ year, month, mday, hour, minute,
726
+ )
727
+ datestring2 = "20%02d-%02d-%02d %02d:%02d:00" % (
728
+ year, month, mday, hour, minute,
729
+ )
730
+ value = float(datestring) # C++: atof(datestring)
731
+ value_string = datestring2
732
+ ascii_value = 2
733
+
734
+ elif data_coding_type == 7:
735
+ # case 7 (C++ line 554-578): date type G, 2 bytes
736
+ date = data[index:index + length]
737
+ if len(date) < 2:
738
+ raise _OutOfBounds(len(date))
739
+ if (date[1] & 0x0F) <= 12:
740
+ # month > 12 = invalid (C++ line 560)
741
+ year = ((date[0] & 0xE0) >> 5) | ((date[1] & 0xF0) >> 1)
742
+ month = date[1] & 0x0F
743
+ mday = date[0] & 0x1F
744
+ datestring = "20%02d%02d%02d" % (year, month, mday)
745
+ datestring2 = "20%02d-%02d-%02d" % (year, month, mday)
746
+ value = float(datestring) # C++: atof(datestring)
747
+ value_string = datestring2
748
+ ascii_value = 2
749
+
750
+ if not switch_again:
751
+ break
752
+
753
+ # C++ line 582: index += len;
754
+ index += length
755
+
756
+ # ------------------------------------------------------------ #
757
+ # scaling - MBusinoLib.cpp line 584-606
758
+ # ------------------------------------------------------------ #
759
+ scaled = 0.0
760
+ scalar = 0
761
+ if def_index != 0:
762
+ # C++ line 587-589 (uint32_t arithmetic, result truncated
763
+ # to int8_t - see _to_int8):
764
+ # scalar = vif_defs[def].scalar + vif - vif_defs[def].base + extensionScaler
765
+ _code, base, _size, def_scalar = VIF_DEFS[def_index]
766
+ scalar = _to_int8(def_scalar + vif - base + extension_scaler)
767
+
768
+ if data_coding_type == 3:
769
+ # real floating point: only the VIF scaling value counts
770
+ scaled = value_float
771
+ if vifarray[0] != 0xFF:
772
+ # C++ line 591-596: first multiply by 10,
773
+ # then (when scalar is negative) divide by 10 and
774
+ # finally add the additive constant - the ORDER is
775
+ # exactly like the original so the result stays
776
+ # bit-identical (division and multiplication do not commute).
777
+ for _ in range(scalar if scalar > 0 else 0):
778
+ scaled *= 10.0
779
+ for _ in range(-scalar if scalar < 0 else 0):
780
+ scaled /= 10.0
781
+ scaled = scaled + extension_additive_constant
782
+ elif vifarray[0] == 0xFF:
783
+ # manufacturer specific: do NOT scale, do NOT add
784
+ scaled = float(value) # C++ line 599-600
785
+ else:
786
+ scaled = float(value)
787
+ for _ in range(scalar if scalar > 0 else 0):
788
+ scaled *= 10.0
789
+ for _ in range(-scalar if scalar < 0 else 0):
790
+ scaled /= 10.0
791
+ scaled = scaled + extension_additive_constant
792
+
793
+ # ------------------------------------------------------------ #
794
+ # build the record - MBusinoLib.cpp line 608-646
795
+ # ------------------------------------------------------------ #
796
+ # The key order matches the C++ writing order exactly.
797
+ record = {}
798
+ # C++ line 610: String("0x" + String(vif,HEX)) -> lower-case
799
+ # hex without leading zeros
800
+ record["vif"] = "0x%x" % vif
801
+ record["code"] = VIF_DEFS[def_index][0]
802
+
803
+ if ascii_value != 1:
804
+ # C++ line 616-620: plain numeric value (not for ASCII)
805
+ record["value_scaled"] = scaled
806
+ if ascii_value > 0:
807
+ # C++ line 621-623: formatted string (date/ASCII)
808
+ record["value_string"] = value_string
809
+
810
+ if if_custom_vif:
811
+ # C++ line 624-626: unit comes from the ASCII VIF
812
+ record["units"] = custom_vif
813
+ elif self.get_code_units(VIF_DEFS[def_index][0]) is not None and not no_unit:
814
+ # C++ line 627-629: unit from the lookup table;
815
+ # "noUnit" switches the unit off (e.g. for _TimeSt)
816
+ record["units"] = self.get_code_units(VIF_DEFS[def_index][0])
817
+
818
+ record["name"] = (
819
+ self.get_code_name(VIF_DEFS[def_index][0]) + name_extension + function_field
820
+ )
821
+
822
+ if sub_unit > 0:
823
+ record["subUnit"] = sub_unit
824
+ if storage_number > 0:
825
+ record["storage"] = storage_number
826
+ if tariff > 0:
827
+ record["tariff"] = tariff
828
+
829
+ records.append(record)
830
+
831
+ # ------------------------------------------------------------ #
832
+ # trailing markers - MBusinoLib.cpp line 648-655
833
+ # ------------------------------------------------------------ #
834
+ # Check the byte that FOLLOWS the record. In C++ `index != size`
835
+ # comes AFTER the read (so one byte is always read, even
836
+ # when index is already at the end - with the usual
837
+ # parameter that byte is the checksum).
838
+ following = rd(index) if index < buf_len else None
839
+ if following == 0x0F and index != size:
840
+ # 0x0F: manufacturer specific data starts here -> stop
841
+ break
842
+ if following == 0x1F and index != size:
843
+ # 0x1F: the next telegram carries more records
844
+ record["telegramFollow"] = 1
845
+ break
846
+
847
+ except _OutOfBounds:
848
+ # Corresponds to the "return 0" spots of the original; there the
849
+ # loop ends immediately, records already appended stay in the
850
+ # JsonArray (and are returned here).
851
+ self._error = MBUS_ERROR.BUFFER_OVERFLOW
852
+ return records
853
+
854
+ return records
855
+
856
+ # ------------------------------------------------------------------
857
+ # Header
858
+ # ------------------------------------------------------------------
859
+ def decode_header(self, buffer, length: int = None) -> dict:
860
+ """Corresponds to MBusinoLib::decodeHeader() in MBusinoLib.cpp line 1563-1677.
861
+
862
+ Decodes the fixed data header of a long frame telegram.
863
+
864
+ :param buffer: the COMPLETE telegram including start/stop byte
865
+ :param length: total length that may be read
866
+ (``None`` = ``len(buffer)``). The original expects
867
+ the ``packet_size`` (``data[1] + 6``) here - as in
868
+ ``decode_telegram()`` and in the examples - NOT the
869
+ file length.
870
+ :return: dict with the ArduinoJson keys ``len``, ``c_field``,
871
+ ``address``, ``ci_field`` and - when CI is 0x72/0x76 - ``id``,
872
+ ``manufacturer``, ``version``, ``medium_code``, ``medium``,
873
+ ``access_counter``, ``status``, ``status_details`` (nested)
874
+ and ``signature``.
875
+ Mapping to the boolean C++ return value:
876
+ * ``{}`` -> C++ ``false`` before writing (too short,
877
+ wrong start byte or unequal length fields)
878
+ * dict without ``"id"`` -> C++ ``false`` AFTER writing
879
+ (CI is not 0x72/0x76, only the basic fields are present)
880
+ * dict with ``"id"`` -> C++ ``true``
881
+ """
882
+ data = bytes(buffer)
883
+ if length is None:
884
+ length = len(data)
885
+
886
+ # 1. minimum length for a long frame with fixed data header (C++ line 1570-1574)
887
+ if length < 19 or len(data) < 19:
888
+ return {}
889
+ # 2. check the start bytes 68 ... 68 (C++ line 1576-1579)
890
+ if data[0] != 0x68 or data[3] != 0x68:
891
+ return {}
892
+ # 3. both length fields must be identical (C++ line 1581-1584)
893
+ if data[1] != data[2]:
894
+ return {}
895
+
896
+ header = {
897
+ "len": data[1], # C++ line 1587
898
+ "c_field": data[4], # C++ line 1588
899
+ "address": data[5], # C++ line 1589 (the C++ README calls this "a_field"!)
900
+ "ci_field": data[6], # C++ line 1590
901
+ }
902
+
903
+ # Only CI 0x72 (long/variable) and 0x76 (short/variable) carry the
904
+ # fixed data header (C++ line 1594-1596)
905
+ if data[6] not in (0x72, 0x76):
906
+ return header # -> corresponds to `return false` after writing
907
+
908
+ # 4. identification number, 4 byte BCD, LSB first (C++ line 1599-1602)
909
+ header["id"] = "%02X%02X%02X%02X" % (data[10], data[9], data[8], data[7])
910
+ # 5. manufacturer, 2 byte, LSB first -> 3 letters of 5 bits each (line 1605-1612)
911
+ manuf_code = (data[12] << 8) | data[11]
912
+ header["manufacturer"] = "".join(
913
+ chr(((manuf_code >> shift) & 0x001F) + 64) for shift in (10, 5, 0)
914
+ )
915
+ # 6. version (C++ line 1615)
916
+ header["version"] = data[13]
917
+ # 7. medium as code and as text (C++ line 1618-1636)
918
+ medium = data[14]
919
+ header["medium_code"] = medium
920
+ header["medium"] = MEDIUM_NAMES.get(medium, "Unknown")
921
+ # 8. transmission counter (C++ line 1639)
922
+ header["access_counter"] = data[15]
923
+ # 9. status byte incl. bit breakdown (C++ line 1642-1651)
924
+ status = data[16]
925
+ header["status"] = status
926
+ app_error = status & 0x03
927
+ status_details = {
928
+ "battery_low": bool(status & 0x04),
929
+ "permanent_error": bool(status & 0x08),
930
+ "temporary_error": bool(status & 0x10),
931
+ }
932
+ # QUIRK: 0x03 (reserved) gets NO app_status key at all in the original
933
+ if app_error == 0x00:
934
+ status_details["app_status"] = "no_error"
935
+ elif app_error == 0x01:
936
+ status_details["app_status"] = "busy"
937
+ elif app_error == 0x02:
938
+ status_details["app_status"] = "any_error"
939
+ header["status_details"] = status_details
940
+ # 10. signature, 2 byte, LSB first (C++ line 1654-1656)
941
+ header["signature"] = (data[18] << 8) | data[17]
942
+
943
+ return header
944
+
945
+ # ------------------------------------------------------------------
946
+ # NEW: complete telegram
947
+ # ------------------------------------------------------------------
948
+ def decode_telegram(self, raw) -> dict:
949
+ """NEW (not in C++): splits a complete M-Bus telegram apart.
950
+
951
+ Frame rules like ``test_decode.cpp``:
952
+
953
+ * layout ``68 LL LL 68 C A CI ... CHK 16``
954
+ * ``packet_size = data[1] + 6`` (test_decode.cpp line 51: 4 bytes
955
+ start/length + L payload + checksum + stop byte)
956
+ * records start at offset ``0x13`` (test_decode.cpp line 53:
957
+ "Standard: skip header + fixed data header")
958
+ * record length ``packet_size - 0x13 - 2`` (test_decode.cpp line 64:
959
+ the 2 are checksum and stop byte)
960
+
961
+ :param raw: telegram as ``bytes``/``bytearray``/list of values or
962
+ as hex string ("68 56 56 68 ...", commas and "0x" prefixes
963
+ are allowed - see ``_parse_hex``)
964
+ :return:::
965
+
966
+ {
967
+ "frame": {...}, # frame checks, see below
968
+ "header": {...}, # result of decode_header()
969
+ "records": [...], # result of decode_records()
970
+ "error": <int>, # MBUS_ERROR code of the decoding step
971
+ "error_message": <str> # English message for the first problem found
972
+ }
973
+
974
+ ``frame`` contains ``byte_count``, ``packet_size``, ``slave_address``,
975
+ ``c_field``, ``ci_field``, ``start_ok``, ``length_ok``, ``fits``,
976
+ ``stop_ok``, ``checksum``, ``checksum_received``, ``checksum_ok``,
977
+ ``quartet_checksum_ok``, ``ok`` and ``error_message``.
978
+
979
+ Error logic: ``error`` carries the MBUS_ERROR code (the enum has no
980
+ frame error code of its own; only "length does not fit" is reported
981
+ as ``BUFFER_OVERFLOW``, because test_decode.cpp also aborts on that
982
+ case with "declared length ... but only N bytes provided").
983
+ ``error_message`` always names the first problem found - when only
984
+ the checksum is wrong, ``error`` stays 0 but the message still
985
+ describes the problem and ``frame["ok"]`` is ``False``.
986
+
987
+ Example - a minimal valid telegram, i.e. the smallest possible frame
988
+ ``68 0F 0F 68 | 08 00 72 | 12 header bytes | CHK 16`` (EFE heat meter
989
+ header, fixed header only, no records, checksum 0xC8 = byte sum)::
990
+
991
+ >>> hex_str = "68 0F 0F 68 08 00 72 09 34 75 73 C5 14 00 0D 43 00 00 00 C8 16"
992
+ >>> MBusinoLib().decode_telegram(hex_str)["frame"]["ok"]
993
+ True
994
+ """
995
+ data = bytes(raw) if not isinstance(raw, str) else _parse_hex(raw)
996
+ # Clear the error before every new message - in C++ getError()
997
+ # empties the state after reading, here we deliver it directly.
998
+ self._error = MBUS_ERROR.NO_ERROR
999
+
1000
+ # --- initial values for the frame checks ---------------------
1001
+ byte_count = len(data)
1002
+ too_short = byte_count < 6 # test_decode.cpp line 37-40
1003
+ packet_size = None
1004
+ start_ok = False
1005
+ length_ok = False
1006
+ fits = False
1007
+ stop_ok = False
1008
+ checksum = None
1009
+ checksum_received = None
1010
+ checksum_ok = False
1011
+ quartet_checksum_ok = False
1012
+ frame_error = ""
1013
+ error = MBUS_ERROR.NO_ERROR
1014
+ header: dict = {}
1015
+ records: list = []
1016
+
1017
+ if too_short:
1018
+ frame_error = "Telegram too short: %d bytes, at least 6 required." % byte_count
1019
+ error = MBUS_ERROR.BUFFER_OVERFLOW
1020
+ else:
1021
+ # packet size: 68 | LL | LL | 68 | C | A | CI | data | CHK | 16
1022
+ # -> 4 header bytes + LL bytes (C up to end of data) + CHK + 16
1023
+ packet_size = data[1] + 6 # test_decode.cpp line 51
1024
+ start_ok = data[0] == 0x68 and data[3] == 0x68 # test_decode does NOT check this
1025
+ length_ok = data[1] == data[2] # both length fields identical
1026
+ fits = packet_size <= byte_count # test_decode.cpp line 56-60
1027
+ if fits:
1028
+ # checksum: byte in front of the stop byte
1029
+ # C++/EN 13757: plain byte sum from the C field (offset 4)
1030
+ # up to the last data byte (offset packet_size-3)
1031
+ checksum = sum(data[4:packet_size - 2]) & 0xFF
1032
+ checksum_received = data[packet_size - 2]
1033
+ checksum_ok = checksum_received == checksum
1034
+ # quartet sum (sum of all half bytes) - used by some
1035
+ # implementations, accepted here as an alternative
1036
+ quartet = 0
1037
+ for byte in data[4:packet_size - 2]:
1038
+ quartet += (byte & 0x0F) + (byte >> 4)
1039
+ quartet_checksum_ok = checksum_received == (quartet & 0xFF)
1040
+ stop_ok = data[packet_size - 1] == 0x16
1041
+
1042
+ # --- name the first problem --------------------------------
1043
+ if not start_ok:
1044
+ frame_error = "Unexpected start byte: 0x%02X/0x%02X instead of 0x68/0x68." % (data[0], data[3])
1045
+ elif not length_ok:
1046
+ frame_error = "Length fields differ: L1=0x%02X, L2=0x%02X." % (data[1], data[2])
1047
+ elif not fits:
1048
+ frame_error = ("Declared length %d expects %d bytes, "
1049
+ "but only %d bytes present." % (data[1], packet_size, byte_count))
1050
+ error = MBUS_ERROR.BUFFER_OVERFLOW
1051
+ elif not stop_ok:
1052
+ frame_error = "Stop byte missing: 0x%02X instead of 0x16." % data[packet_size - 1]
1053
+ elif not checksum_ok and not quartet_checksum_ok:
1054
+ frame_error = ("Checksum mismatch: expected 0x%02X, "
1055
+ "received 0x%02X." % (checksum, checksum_received))
1056
+
1057
+ # --- decode (like test_decode.cpp, independent of -----------
1058
+ # start byte/checksum - the original does not check those) ----
1059
+ if fits:
1060
+ header = self.decode_header(data, packet_size) # C++ decodeHeader()
1061
+ # data from offset 0x13, without checksum(1) and stop byte(1)
1062
+ start_address = 0x13 # test_decode.cpp line 53
1063
+ data_len = max(0, packet_size - start_address - 2) # test_decode.cpp line 64
1064
+ records = self.decode_records(data[start_address:packet_size], data_len)
1065
+ error = self._error
1066
+
1067
+ if isinstance(error, IntEnum):
1068
+ error = int(error)
1069
+
1070
+ # Error message: the text of an MBUS_ERROR if there is one, otherwise
1071
+ # the frame hint - "No error" only when everything really fitted.
1072
+ if error != MBUS_ERROR.NO_ERROR:
1073
+ error_message = self.get_error_message(error)
1074
+ elif frame_error:
1075
+ error_message = frame_error
1076
+ else:
1077
+ error_message = "No error"
1078
+
1079
+ frame = {
1080
+ "byte_count": byte_count,
1081
+ "packet_size": packet_size,
1082
+ "slave_address": data[5] if not too_short else None,
1083
+ "c_field": data[4] if not too_short else None,
1084
+ "ci_field": data[6] if not too_short else None,
1085
+ "start_ok": start_ok,
1086
+ "length_ok": length_ok,
1087
+ "fits": fits,
1088
+ "stop_ok": stop_ok,
1089
+ "checksum": checksum,
1090
+ "checksum_received": checksum_received,
1091
+ "checksum_ok": checksum_ok,
1092
+ "quartet_checksum_ok": quartet_checksum_ok,
1093
+ "ok": (not too_short) and start_ok and length_ok and fits and stop_ok
1094
+ and (checksum_ok or quartet_checksum_ok),
1095
+ "error_message": frame_error,
1096
+ }
1097
+
1098
+ return {
1099
+ "frame": frame,
1100
+ "header": header,
1101
+ "records": records,
1102
+ "error": error,
1103
+ "error_message": error_message,
1104
+ }
1105
+
1106
+
1107
+ # ---------------------------------------------------------------------------
1108
+ # DIF table (C++: switch(difLeast4bit) in MBusinoLib.cpp line 98-162)
1109
+ # index = low 4 bits of the DIF, value = (length in bytes, data coding type)
1110
+ # type: 0 = no data, 1 = integer, 2 = BCD, 3 = real,
1111
+ # 4 = variable, 5 = special function
1112
+ # ---------------------------------------------------------------------------
1113
+ _DIF_TABLE = (
1114
+ (0, 0), # 0x00 -> No data (C++ line 99)
1115
+ (1, 1), # 0x01 -> 8 Bit Integer (C++ line 102)
1116
+ (2, 1), # 0x02 -> 16 Bit Integer (C++ line 105)
1117
+ (3, 1), # 0x03 -> 24 Bit Integer (C++ line 108)
1118
+ (4, 1), # 0x04 -> 32 Bit Integer (C++ line 111)
1119
+ (4, 3), # 0x05 -> 32 Bit Real (C++ line 114)
1120
+ (6, 1), # 0x06 -> 48 Bit Integer (C++ line 117)
1121
+ (8, 1), # 0x07 -> 64 Bit Integer (C++ line 120)
1122
+ (0, 0), # 0x08 -> Selection for Readout, not supported (C++ line 123)
1123
+ (1, 2), # 0x09 -> 2 digit BCD (C++ line 126)
1124
+ (2, 2), # 0x0A -> 4 digit BCD (C++ line 129)
1125
+ (3, 2), # 0x0B -> 6 digit BCD (C++ line 132)
1126
+ (4, 2), # 0x0C -> 8 digit BCD (C++ line 135)
1127
+ (0, 4), # 0x0D -> variable length (C++ line 138)
1128
+ (6, 2), # 0x0E -> 12 digit BCD (C++ line 141)
1129
+ (0, 5), # 0x0F -> Special Functions (C++ line 144)
1130
+ )
1131
+
1132
+ # Function field (C++: switch(difFunctionField) in MBusinoLib.cpp line 164-179)
1133
+ # bits 4+5 of the DIF: 0=current, 1=_max, 2=_min, 3=_err
1134
+ _FUNCTION_FIELDS = ("", "_max", "_min", "_err")
1135
+
1136
+ # Name suffixes for the "reading user" VIFE 0xFC/0xFF
1137
+ # (C++: switch(vifExtensionBuffer) in MBusinoLib.cpp line 335-371)
1138
+ _PHASE_SUFFIX = {
1139
+ 1: "_L1",
1140
+ 2: "_L2",
1141
+ 3: "_L3",
1142
+ 4: "_N",
1143
+ 5: "_L1-L2",
1144
+ 6: "_L2-L3",
1145
+ 7: "_L3-L1",
1146
+ 10: "_abs.",
1147
+ 12: "_delta",
1148
+ }