M-Bus Gateway
← Tilbage til blog
· OMS· telegram· wM-Bus· DIF· VIF· protokol· parsing· EN 13757· teknisk

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-feltBeskrivelse
0x44SND_NR (Send without request — standard C1/T1)
0x72Variable length data record (fuldt telegram)
0x78Variable length, fabrikantspecifik
0x7AShort 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

FormatKendetegnParser-support
OMS (EN 13757-4)CI=0x72/0x7A, standard DIF/VIFwmbusmeters auto
Techem proprietærCI=0xA1/0xA2, eget formatwmbusmeters teca/techem_hca_typ8
Kamstrup multicalOMS + fabrikant-VIFEwmbusmeters multical302
Brunata/ApatorSemi-OMS, eget extended VIFwmbusmeters 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.

Se wmbusmeters installationsguide eller OMS standard-guide.