OMS-telegram struktur forklaret — fra rå bytes til forbrugsdata
Teknisk gennemgang af OMS-telegram (EN 13757-4): frame-struktur, DIF/VIF-felter, fabrikantspecifikke blokke, AES-kryptering og parsing med wmbusmeters.
Af M-Bus Gateway
Et wM-Bus OMS-telegram er en datastruktur defineret i EN 13757-4. Forståelsen af telegram-strukturen er nødvendig for fejlfinding, integration og fabrikantspecifik parsing.
OMS-telegram overblik
[Header] [Application Header] [Data Records] [CRC]
Eksempel hex (Engelmann SensoStar, C1, ukrypteret):
4E 44 A5 21 12 34 56 78 | 7A 15 00 00 | 00 | 04 06 XX XX XX XX | 44 06 XX XX XX XX | 02 FD 17 XX XX | ...
Opdeling:
4E = Telegram-længde (78 bytes)
44 = CI-felt (SND_NR = uden svar-krav)
A5 21 = Fabrikant-ID (ENG = Engelmann, 2 bytes little-endian)
12345678 = Serienummer (4 bytes little-endian)
7A = Version + device-type
15 = Device type: 0x15 = heat_cost_allocator
00 00 = Access number + status
CI-felt (Control Information)
| CI-felt | Beskrivelse |
|---|---|
| 0x44 | SND_NR (Send without request — standard C1/T1) |
| 0x72 | Variable length data record (fuldt telegram) |
| 0x78 | Variable length, fabrikantspecifik |
| 0x7A | Short transport layer (kompakt) |
Fabrikant-ID encoding
Fabrikant-ID er 2 bytes little-endian encoded som 3 ASCII-bogstaver:
def decode_manufacturer(raw: bytes) -> str:
"""Decode 2-byte manufacturer ID to 3-letter FAB ID."""
val = raw[0] | (raw[1] << 8)
c1 = chr((val >> 10 & 0x1F) + 64)
c2 = chr((val >> 5 & 0x1F) + 64)
c3 = chr((val & 0x1F) + 64)
return c1 + c2 + c3
# Eksempler:
# 0xA521 → "ENG" (Engelmann)
# 0x2C2D → "KAM" (Kamstrup)
# 0x5C2D → "TCH" (Techem)
# 0x8614 → "APT" (Apator/Brunata)
DIF/VIF — Data Information Field / Value Information Field
Data records i OMS-telegrammer bruger DIF/VIF-par til at beskrive hvert datapunkt:
DIF (Data Information Field)
Bit 7: Extension bit (1 = yderligere DIF-felt følger)
Bit 6: LSB of storage number
Bit 4-5: Function field (00=instant, 01=max, 10=min, 11=error)
Bit 0-3: Data type
Data type:
0x00 = No data
0x01 = 8-bit integer
0x02 = 16-bit integer
0x03 = 24-bit integer
0x04 = 32-bit integer
0x05 = 32-bit float
0x06 = 48-bit integer
0x09 = BCD 2 digits
0x0C = BCD 8 digits (4 bytes)
0x0D = Variable length
0x0F = Special functions
VIF (Value Information Field)
Bit 7: Extension bit
Bit 0-6: Value type and unit
Fælles VIF-værdier:
0x06 = Energi (10^-1 Wh → kWh ved multiplikator)
0x13 = Volumen (m³)
0x14 = Volumen (ft³)
0x2B = Power (W)
0x59 = Fremløbstemperatur (0.01°C)
0x5D = Returtemperatur (0.01°C)
0x61 = Temperaturdifferens (mK)
0x6C = Dato og tid
0xFD = Fabrikant-specifik følger
Komplet telegram-parsing (Python)
def parse_oms_data_records(payload: bytes) -> list[dict]:
"""Parse DIF/VIF data records fra OMS-telegram payload."""
records = []
i = 0
while i < len(payload):
dif = payload[i]; i += 1
if dif == 0x0F: # Manufacturer specific — stop
break
# Extended DIF:
while dif & 0x80:
dif = payload[i]; i += 1
vif = payload[i]; i += 1
# Extended VIF:
while vif & 0x80:
vif = payload[i]; i += 1
data_type = dif & 0x0F
length = _get_dif_length(data_type)
raw_value = payload[i:i+length]; i += length
value = _decode_value(raw_value, data_type)
unit, multiplier = _decode_vif(vif)
records.append({
"dif": hex(dif),
"vif": hex(vif),
"value": value * multiplier if multiplier else value,
"unit": unit,
})
return records
def _get_dif_length(data_type: int) -> int:
lengths = {0x01: 1, 0x02: 2, 0x03: 3, 0x04: 4,
0x05: 4, 0x06: 6, 0x09: 1, 0x0C: 4, 0x0D: 0}
return lengths.get(data_type, 0)
Kamstrup Multical 302 — telegram-eksempel
Data records (ukrypteret C1-telegram):
04 06 B8 0B 00 00 → DIF=0x04 (32-bit int), VIF=0x06 (Wh, ×10^-1) = 3000 kWh (akk.)
44 06 40 06 00 00 → DIF=0x44 (32-bit, periodeslut), VIF=0x06 = 1600 kWh (forrige periode)
6C D1 27 → DIF=0x6C (dato), VIF=0x6C → periodeslutt-dato
02 59 B4 08 → DIF=0x02 (16-bit), VIF=0x59 → fremløb = 22.60°C
02 5D 28 07 → DIF=0x02 (16-bit), VIF=0x5D → retur = 18.48°C
02 61 8C 01 → DIF=0x02 (16-bit), VIF=0x61 → ΔT = 39.24 K × 0.01 = 0.39 K?
AES-128 CTR kryptering
Krypterede OMS-telegrammer bruger AES-128 i CTR mode:
from Crypto.Cipher import AES
def decrypt_oms_ctr(payload: bytes, key: bytes, meter_id: bytes, mfct: bytes) -> bytes:
"""Dekryptér OMS AES-128 CTR payload."""
# IV = meter_id (4 bytes) + manufacturer (2 bytes) + version (1) + device_type (1) + ...
iv = meter_id + mfct + b'\x00' * 8 # Forenklet — faktisk IV beregning er standardiseret
cipher = AES.new(key, AES.MODE_CTR, nonce=b'', initial_value=iv)
return cipher.decrypt(payload)
wmbusmeters håndterer dette automatisk — du behøver kun angive AES-nøglen i konfigurationsfilen.
Proprietary vs OMS
| Format | Kendetegn | Parser-support |
|---|---|---|
| OMS (EN 13757-4) | CI=0x72/0x7A, standard DIF/VIF | wmbusmeters auto |
| Techem proprietær | CI=0xA1/0xA2, eget format | wmbusmeters teca/techem_hca_typ8 |
| Kamstrup multical | OMS + fabrikant-VIFE | wmbusmeters multical302 |
| Brunata/Apator | Semi-OMS, eget extended VIF | wmbusmeters apator162 |
Debugging med wmbusmeters
# Se rå hex-telegram:
wmbusmeters --listento=c1 /dev/wmbus NOKEY 2>&1 | grep -o '"frame":"[^"]*"'
# Fuld verbose parsing:
wmbusmeters --debug --listento=c1 /dev/wmbus NOKEY 2>&1 | grep -A20 "12345678"
# Test-parsing af hex-telegram:
echo "4E44A521..." | wmbusmeters --analyze
Konklusion
OMS-telegram-strukturen er baseret på EN 13757-4 med DIF/VIF data records. De fleste moderne HCA'er og varmemålere er OMS-kompatible. wmbusmeters parser automatisk standardformatet og de mest udbredte fabrikantspecifikke varianter. Kun ældre Techem-typer kræver særlig driver.