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.
- MBusinoLibPy/__init__.py +68 -0
- MBusinoLibPy/enums.py +170 -0
- MBusinoLibPy/lookup_tables.py +782 -0
- MBusinoLibPy/mbusino_lib.py +1148 -0
- MBusinoLibPy/py.typed +0 -0
- MBusinoLibPy/vif_defs.py +174 -0
- mbusinolibpy-0.10.0.dist-info/METADATA +284 -0
- mbusinolibpy-0.10.0.dist-info/RECORD +11 -0
- mbusinolibpy-0.10.0.dist-info/WHEEL +5 -0
- mbusinolibpy-0.10.0.dist-info/licenses/LICENSE +674 -0
- mbusinolibpy-0.10.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
}
|