OmniPower implementation¶
Parse Kamstrup OmniPower wm-bus telegrams¶
- platform
Python 3.5.10 on Linux, OS X
- synopsis
Implements parsing functionality for C1 telegrams and log handling for data series
- author
Janus Bo Andersen
- date
28 October 2020
Version history¶
Ver 1.0: Set up parser and decryption. Janus.
Ver 2.0: Implement CRC16, timezone. Janus.
Ver 2.1: More robust exception handling, parse ELL-SN. Janus
Ver 2.2: Utilize new MeterMeasurement.is_empty() in validation during parsing. Janus
Overview¶
This module implements parsing for the Kamstrup OmniPower meter, single-phase.
The meter sends wm-bus C1 (compact one-way) telegrams.
Telegrams on wm-bus are little-endian, i.e. LSB first.
The meter sends 1 long and 7 short telegrams, and then repeats.
Long telegrams include data record headers (DRH) and data, that is DIF/VIF codes + data.
Short telegrams only include data.
Telegram fields¶
In a telegram C1 telegram, the data fields are:
# |
Byte# |
Bytes |
M-bus field |
Description |
Expected value (little-endian) |
|---|---|---|---|---|---|
0 |
0 |
1 |
L |
Telegram length |
0x27 (39 bytes, short frame), or 0x2D (45 bytes, long frame) |
1 |
1 |
1 |
C |
Control field (type and purpose of message) |
0x44 (SND_NR) |
2 |
2-3 |
2 |
M |
Manufacturer ID (official ID code) |
0x2D2C (KAM) |
3 |
4-7 |
4 |
A |
Address (meter serial number) |
0x57686632 (big-endian:32666857) |
4 |
8 |
1 |
Ver. |
Version number of the wm-bus firmware |
0x30 |
5 |
9 |
1 |
Medium |
Type / medium of meter |
0x02 (Electricity) |
6 |
10 |
1 |
CI |
Control Information |
0x8D (Extended Link Layer 2) |
7 |
11 |
1 |
CC |
Communication Control |
0x20 (Slow response sync.) |
8 |
12 |
1 |
ACC |
Access field |
Varies |
9 |
13-16 |
4 |
AES CTR / SN |
AES counter (Session number) |
Varies, see below |
10 |
17-39 17-45 |
23 29 |
Data |
Contains AES-encrypted data frame, varying for short and long frames |
Encrypted data |
11 |
2 |
CRC16 |
CRC16 check |
The fields 0-9 of the telegram can be unpacked using the little-endian format <BBHIBBBBB, where
< marks little-endian,
B is an unsigned 1 byte (char),
H is an unsigned 2 byte (short),
I is an unsigned 4 byte (int)
AES counter / Session number¶
The AES_CTR / Extended Link Layer SN field (ELL-SN) is is structured as per EN 13757-4, p. 54.
The example shows ELL-SN value of 0x01870320 (little-endian) -> 0x20038701 (big-endian). Bit readout:
Byte: |
3 |
2 |
1 |
0 |
|---|---|---|---|---|
Hex: |
0x20 |
0x03 |
0x87 |
0x01 |
Binary: |
0010 0000 |
0000 0011 |
1000 0111 |
0000 0001 |
Should give the following slicing and interpretation:
Bits: |
31-29 |
28-04 |
03-00 |
|---|---|---|---|
Field: |
ENC |
Time |
Session |
Example: |
001 |
0 0000 0000 0011 1000 0111 0000 |
0001 |
Hex: |
0x1 |
0x3870 (14448) |
0x1 |
ENC (Encryption): 0 -> No encryption, 1 -> AES-CTR mode, higher -> reserved.
Time: Minute counter since 01/01/2013 (?), or since meter started, requires RTC set on meter. So time measurement was taken about 10 days after the meter was started.
Session: Incremented by meter for each transmission, unless using partial/fractured frames.
Parsing. The whole ELL-SN field is read out and masks are used to extract fields.
Telegram examples¶
Encrypted short telegrams:
L |
C |
M |
A |
Ver |
Med |
CI |
CC |
ACC |
AES CTR |
Encrypted payload |
CRC 16 |
|---|---|---|---|---|---|---|---|---|---|---|---|
27 |
44 |
2D 2C |
5768 6632 |
30 |
02 |
8D |
20 |
2E |
2187 0320 |
D3A4F149 B1B8F578 3DF7434B 8A66A557 86499ABE 7BAB59 |
xxxx |
27 |
44 |
2d 2c |
5768 6632 |
30 |
02 |
8d |
20 |
63 |
60dd 0320 |
c42b87f4 6fc048d4 2498b44b 5e34f083 e93e6af1 617631 |
3d9c |
27 |
44 |
2d 2c |
5768 6632 |
30 |
02 |
8d |
20 |
8e |
11de 0320 |
188851bd c4b72dd3 c2954a34 1be369e9 089b4eb3 858169 |
494e |
Encrypted long telegrams:
L |
C |
M |
A |
Ver |
Med |
CI |
CC |
ACC |
AES CTR |
Encrypted payload |
CRC 16 |
|---|---|---|---|---|---|---|---|---|---|---|---|
2D |
44 |
2D 2C |
5768 6632 |
30 |
02 |
8D |
20 |
64 |
61DD 0320 |
38931d14 b405536e 0250592f 8b908138 d58602ec a676ff79 e0caf0b1 4d |
0e7d |
Decryption¶
The wireless m-bus on OmniPower uses AES-128 Mode CTR (if enabled, otherwise no encryption).
See EN 13757-4:2019, p. 54, as ELL (Ext. Link-Layer) with ENC = 0x1 => AES-CTR.
A decryption prefix (initial counter block) is built from some of the fields.
See table 54 on p. 55 of EN 13757-4:2019.
It can be packed using the format <HIBBBIB.
M |
A |
Ver |
Med |
CC |
AES_CTR |
FN |
BC |
|---|---|---|---|---|---|---|---|
2D2C |
57686632 |
30 |
02 |
20 |
21870320 |
0000 |
00 |
AES Prefix (initialization vector): Fields M, …, AES_CTR, FN.
FN: frame number (frame # sent by meter within same session number, in case of multi-frame transmissions).
AES Counter: BC
BC: Block counter (encryption block number, counts up for each 16 byte block decrypted within the telegram).
Decrypted payload examples¶
The interpretation of the fields in the OmniPower is
Field |
Kamstrup name |
Data fmt (DIF) |
Value type (VIF/E) |
VIF/E meaning |
DIF VIF/E |
|---|---|---|---|---|---|
Data 1 |
A+ |
32-bit uint |
Energy, 10^1 Wh |
Consumption from grid, accum. |
04 04 |
Data 2 |
A- |
32-bit uint |
Energy, 10^1 Wh |
Production to grid, accum. |
04 84 3C |
Data 3 |
P+ |
32-bit uint |
Power, 10^0 W |
Consumption from grid, instantan. |
04 2B |
Data 4 |
P- |
32-bit uint |
Power, 10^0 W |
Production to grid, instantan. |
04 AB 3C |
Transport layer control information fields (TPL-CI), ref. EN 13757-7:2018, p. 17, introduce Application Layer (APL) as:
0x78 with full frames (Response from device, full M-Bus frame)
0x79 with compact frames (Response from device, M-Bus compact frame)
Data integrity check (CRC16)¶
The first 2 bytes (16 bits) of a payload is always the CRC16 value of the sent message. This value must be checked versus CRC16 calculated on the received payload.
Decrypted short telegram payload¶
CRC16 |
TPL-CI |
Data fmt. sign. |
CRC16 data |
Data 1 |
Data 2 |
Data 3 |
Data 4 |
|---|---|---|---|---|---|---|---|
1170 |
79 |
138C |
4491 |
CE000000 |
00000000 |
03000000 |
00000000 |
Measurement data starts at byte 7, and can easily be extracted using <IIII little-endian format.
In this example, 206 10^1 Wh (2.06 kWh) have been consumed, and the current power draw is 3 10^0 W (0.003 kW).
Decrypted long telegram payload¶
In this kind of telegram, the DRHs are included.
CRC16 |
TPL-CI |
DIF/VIF 1 |
Data 1 |
DIF/VIF/VIFE 2 |
Data 2 |
DIF/VIF 3 |
Data 3 |
DIF/VIF 4 |
Data 4 |
|---|---|---|---|---|---|---|---|---|---|
9831 |
78 |
04 04 |
D7000000 |
04 84 3C |
00000000 |
04 2B |
03000000 |
04 AB 3C |
00000000 |
Extraction is slightly more complex, requiring either a longer parsing pattern or perhaps a regex.
In this example, 215 10^1 Wh (2.15 kWh) have been consumed, and the current power draw is 3 10^0 W (0.003 kW).
The C1 Telegram class¶
-
class
meter.OmniPower.C1Telegram(telegram: bytes)[source]¶ Implements capture of data fields for a C1 telegram from OmniPower
-
decrypt_using(meter: meter.OmniPower.OmniPower) → bool[source]¶ Decrypts a telegram and inserts it into telegram (self) Uses a meter object and its key to perform decryption. Requires instantiated OmniPower meter with valid AES-key.
-
The OmniPower class¶
-
class
meter.OmniPower.OmniPower(name: str = 'Kamstrup OmniPower one-phase', meter_id: str = '32666857', manufacturer_id: str = '2C2D', medium: str = '02', version: str = '30', aes_key: str = '9A25139E3244CC2E391A8EF6B915B697')[source]¶ Implementation of our OmniPower single-phase meter Passed values are hex encoded as string, e.g. ‘2C2D’ for value 0x2C2D.
-
add_measurement_to_log(measurement: meter.MeterMeasurement.MeterMeasurement) → bool[source]¶ Pushes a new measurement to the tail end of the log
-
decrypt(telegram: meter.OmniPower.C1Telegram) → bytes[source]¶ Decrypt a telegram. Returns decrypted bytes. Raises CrcCheckException if CRCs do not match after decryption.
Requires:
the prefix from the telegram (telegram.prefix), and
the encryption key stored in the meter object.
Decrypts the data stored in field telegram.encrypted
-
dump_log_to_json() → str[source]¶ Returns a JSON string of all measurement frames in log, with an incremented number for each observation.
-
extract_measurement_frame(telegram: meter.OmniPower.C1Telegram) → meter.MeterMeasurement.MeterMeasurement[source]¶ Requires that the telegram is already decrypted, otherwise returns empty measurement frame.
-
is_this_my(telegram: meter.OmniPower.C1Telegram) → bool[source]¶ Check whether a given telegram is from this meter by comparing meter setting to telegram
-
process_telegram(telegram: meter.OmniPower.C1Telegram) → bool[source]¶ Does entire processing chain for a telegram, including adding to log. Returns True if processing is OK and added to log OK. Otherwise False.
-