M-Bus Gateway
← Tilbage til blog
· wmbusmeters· JSON· parsing· Python· wM-Bus· MQTT· AES· IoT· gateway

wmbusmeters JSON output — parsing og feltmapping i Python

wmbusmeters JSON output format: feltstruktur, fabrikant-specifikke felter, AES-fejl, Python parsing, MQTT integration og diagnostik af manglende målere.

Af M-Bus Gateway

wmbusmeters konverterer rå wM-Bus telegrammer til struktureret JSON. Her er det komplette format og Python-integration.


JSON output format

// Eksempel: Techem HCA telegram (T1 mode)
{
  "media": "heat cost allocator",
  "meter": "techem_hca",
  "name": "hca_stue_lej1",
  "id": "12345678",
  "status": "OK",
  "current_hca": 1247,
  "previous_hca": 4821,
  "current_date": "2026-04-30",
  "previous_date": "2025-04-30",
  "rssi_dbm": -78,
  "timestamp": "2026-05-24T06:00:00Z"
}

// Kamstrup Multical 302 (varme, C1 mode)
{
  "media": "heat",
  "meter": "multical302",
  "name": "varmemaaler_kaelder",
  "id": "87654321",
  "status": "OK",
  "total_energy_consumption_kwh": 52483.2,
  "volume_m3": 2847.1,
  "power_kw": 12.4,
  "flow_temperature_c": 72.3,
  "return_temperature_c": 41.8,
  "rssi_dbm": -65,
  "timestamp": "2026-05-24T06:00:01Z"
}

Konfigurationsfil pr. måler

# /etc/wmbusmeters.d/hca_lej1.conf
name=hca_stue_lej1
id=12345678
driver=techem_hca
key=A1B2C3D4E5F6708192A3B4C5D6E7F800
# /etc/wmbusmeters.d/heat_main.conf
name=varmemaaler_kaelder
id=87654321
driver=multical302
# Ingen key — ikke krypteret

Python parsing af wmbusmeters output

# gateway/src/wmbus/parser.py

import json
import subprocess
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

@dataclass
class WMBusReading:
    meter_id: str
    driver: str
    name: str
    timestamp: datetime
    rssi_dbm: Optional[float]
    status: str
    # Type-specifikke felter
    current_hca: Optional[float] = None
    total_energy_kwh: Optional[float] = None
    volume_m3: Optional[float] = None
    flow_temp_c: Optional[float] = None
    return_temp_c: Optional[float] = None
    power_kw: Optional[float] = None
    battery_level_pct: Optional[float] = None

def parse_wmbus_json(raw: str) -> Optional[WMBusReading]:
    """Parse ét JSON-objekt fra wmbusmeters stdout."""
    try:
        data = json.loads(raw.strip())
    except json.JSONDecodeError:
        return None

    # wmbusmeters sætter status="OK" eller fejlkode
    status = data.get("status", "UNKNOWN")

    return WMBusReading(
        meter_id=data["id"],
        driver=data.get("meter", "unknown"),
        name=data.get("name", data["id"]),
        timestamp=datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")),
        rssi_dbm=data.get("rssi_dbm"),
        status=status,
        current_hca=data.get("current_hca"),
        total_energy_kwh=data.get("total_energy_consumption_kwh"),
        volume_m3=data.get("volume_m3"),
        flow_temp_c=data.get("flow_temperature_c"),
        return_temp_c=data.get("return_temperature_c"),
        power_kw=data.get("power_kw"),
        battery_level_pct=_estimate_battery(data),
    )

def _estimate_battery(data: dict) -> Optional[float]:
    """wmbusmeters leverer battery_v eller battery_remaining_days afhængig af driver."""
    if "remaining_battery_days" in data:
        days = data["remaining_battery_days"]
        return min(100.0, round(days / 3650 * 100, 1))  # antag 10 år = 100%
    if "battery_v" in data:
        v = data["battery_v"]
        # Typisk HCA batteri: 3.6V = 100%, 2.8V = 0%
        return min(100.0, max(0.0, round((v - 2.8) / (3.6 - 2.8) * 100, 1)))
    return None

AES-dekrypteringsfejl

// AES-fejl: forkert nøgle eller manglende nøgle
{
  "media": "heat cost allocator",
  "meter": "techem_hca",
  "name": "hca_stue_lej2",
  "id": "AABBCCDD",
  "status": "DEC_ERR",
  "rssi_dbm": -72,
  "timestamp": "2026-05-24T06:00:02Z"
}

// status-koder:
// "OK"       → telegram modtaget og dekrypteret korrekt
// "DEC_ERR"  → AES-dekrypteringsfejl (forkert nøgle)
// "STUB"     → driver ikke implementeret for denne fabrikant/type
// "ERR"      → generel fejl (checksumfejl, trunkeret telegram)
def is_decryption_error(reading: WMBusReading) -> bool:
    return reading.status == "DEC_ERR"

def is_valid_reading(reading: WMBusReading) -> bool:
    return reading.status == "OK" and (
        reading.current_hca is not None or
        reading.total_energy_kwh is not None
    )

wmbusmeters subprocess integration

# gateway/src/wmbus/listener.py

import asyncio
import subprocess
from pathlib import Path

WMBUS_DEVICE = "/dev/wmbus"
WMBUS_MODE = "c1"

async def run_wmbusmeters(callback) -> None:
    """
    Kør wmbusmeters som subprocess og parse JSON output line-by-line.
    Genstarter automatisk ved crash.
    """
    cmd = [
        "wmbusmeters",
        "--format=json",
        f"--logfile=/var/log/wmbusmeters/wmbusmeters.log",
        f"{WMBUS_DEVICE}:{WMBUS_MODE}",
        "CONF_DIR=/etc/wmbusmeters.d/",
    ]

    while True:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        try:
            async for line in proc.stdout:
                raw = line.decode("utf-8", errors="replace").strip()
                if not raw or not raw.startswith("{"):
                    continue
                reading = parse_wmbus_json(raw)
                if reading:
                    await callback(reading)
        finally:
            proc.kill()
            await proc.wait()
            await asyncio.sleep(5)  # vent inden genstart

MQTT publish med parsed data

# gateway/src/wmbus/publisher.py

import msgpack
import zlib
from datetime import datetime, timezone
from .parser import WMBusReading

def reading_to_payload(readings: list[WMBusReading], gateway_id: str) -> bytes:
    """Konvertér liste af WMBusReading til komprimeret MessagePack payload."""
    data = {
        "gw": gateway_id,
        "ts": datetime.now(timezone.utc).isoformat(),
        "r": [
            {
                "id": r.meter_id,
                "drv": r.driver,
                "nm": r.name,
                "st": r.status,
                "ts": r.timestamp.isoformat(),
                "hca": r.current_hca,
                "kwh": r.total_energy_kwh,
                "m3": r.volume_m3,
                "ft": r.flow_temp_c,
                "rt": r.return_temp_c,
                "kw": r.power_kw,
                "bat": r.battery_level_pct,
                "rssi": r.rssi_dbm,
            }
            for r in readings
        ],
    }
    return zlib.compress(msgpack.packb(data, use_bin_type=True))

Diagnostik: manglende målere

# Se alle modtagne meter-IDs i realtid:
wmbusmeters --format=json /dev/wmbus:c1 | jq -r '.id + " " + .status + " " + .meter'

# Filtrer kun fejl:
wmbusmeters --format=json /dev/wmbus:c1 | jq 'select(.status != "OK")'

# Tæl modtagne telegrammer pr. ID (10 min):
wmbusmeters --format=json /dev/wmbus:c1 2>/dev/null | \
  jq -r '.id' | sort | uniq -c | sort -rn

# Test specifik AES-nøgle:
wmbusmeters --format=json /dev/wmbus:c1 \
  --field-id=12345678 \
  --field-key=A1B2C3D4E5F6708192A3B4C5D6E7F800

Driver-tabel

Driver              Fabrikant        Medie
------------------------------------------------------
techem_hca          Techem           HCA (varmefordeling)
multical302         Kamstrup         Varmemåler
multical403         Kamstrup         Varmemåler
multical21          Kamstrup         Vandmåler
sensostar           Engelmann        Varmemåler + HCA
izar                Diehl            Vandmåler (IZAR R4)
qalcosonic_w1       Axioma           Vandmåler
supercal5           Sontex           Varmemåler
mbus_easy           Zenner           Vandmåler (M-Bus Easy+)

Konklusion

wmbusmeters JSON output er konsistent på tværs af drivere — id, status, rssi_dbm og timestamp er altid til stede. Type-specifikke felter som current_hca eller total_energy_consumption_kwh varierer pr. driver. DEC_ERR indikerer forkert AES-nøgle. Python-parsing via json.loads() + dataclass giver typesikker håndtering i gateway-koden.

Se wmbusmeters installation guide eller AES-128 CTR dekryptering guide.