M-Bus Gateway
← Tilbage til blog
· IoT· SIM-kort· 1NCE· 4G· LTE· dataforbrug· komprimering· gateway· NB-IoT· LTE-M

IoT SIM-kort og dataplan optimering — 10-årig budget med 1NCE

IoT SIM-kort til gateway med 4G LTE: 1NCE 500MB/10-årsplan, dataforbrug per dag, MessagePack+zlib komprimering, fallback-strategi og sammenligning med standard SIM.

Af M-Bus Gateway

M-Bus Gateway bruger 1NCE IoT SIM med 500MB over 10 år. Her er designet der gør det muligt at holde dataforbruget under 200MB over gateway-levetiden.


1NCE: Hvad er det og hvad koster det?

1NCE (onence.com):
  Pris: ~20 USD engangs (lifetime)
  Data: 500 MB over 10 år
  SMS: 250 SMS over 10 år
  Gyldighed: 10 år fra aktivering
  
  Dækning:
  → Roaming i 100+ lande (inkl. hele EU/Norden)
  → LTE-M og NB-IoT (lavstrøm)
  → Standard 4G LTE fallback
  
  APN: iot.1nce.net (1.8V nano-SIM)
  
  Alternativ sammenligning:
  Telia IoT: ~3 kr./MB → 200 MB over 10 år = 600 kr./SIM
  1NCE: ~130 kr. engangs for 500 MB → 90% billigere

Dataforbrug-budget

Daglig payload (06:00 UTC):
  Rå readings (50 målere × 10 felter JSON): ~25 KB
  MessagePack-komprimeret: ~8 KB
  zlib-komprimeret: ~3 KB (62% reduktion)

Status heartbeat (hvert 5. min):
  JSON: {"gateway_id": "GW-0001", "ts": 1716508800, "online": true}
  → 80 bytes × 288 gange/dag = 23 KB/dag ukomprimeret

OTA-opdatering (kvartalvis, 5 MB/opdatering):
  → 4 opdateringer/år = 20 MB/år
  → 200 MB over 10 år (OTA alene!)
  → Optimeret: Binære diff-patches (typisk < 500 KB/patch)

Cloudflare Tunnel (teknikeradgang):
  → On-demand, max 30 min
  → Ca. 10 sessioner/år á 50 MB = 500 MB/år → 5 GB/10 år!
  → Begræns til absolut minimum: Kritisk fejlfinding kun

Data-budget (10-årig plan)

Optimeret dataforbrug:

Daglig payload (3 KB × 365 × 10 år):     10,9 MB
Status heartbeat:
  → Kun ved forbindelsesskift (ikke konstant)
  → 2 × 80 bytes/dag × 3.650 dage:         0,6 MB
OTA-patches (diff, 300 KB × 20 patches):  6,0 MB
Remote debug-sessioner (10/år × 2 MB):   20,0 MB
MQTT overhead (headers, ACK):             10,0 MB
Buffer:                                  152,5 MB
                                        --------
Total:                                  200,0 MB

Resterende buffer: 300 MB (60% reserve)
1NCE limit: 500 MB over 10 år → Komfortabelt

MessagePack + zlib: Komprimerings-implementering

# gateway/src/mqtt/serializer.py
import msgpack
import zlib
from datetime import datetime

def serialize_payload(readings: list[dict], gateway_id: str) -> bytes:
    """
    Komprimér daglig payload til minimal størrelse.
    Reducer JSON overhead med MessagePack + zlib.
    """
    payload = {
        "v": 1,               # Version for bakwards-compat
        "gw": gateway_id,
        "ts": int(datetime.utcnow().timestamp()),
        "r": [
            {
                "id": r["meter_installation_id"],
                "v": r["value"],
                "u": r["unit"],
                "t": int(r["timestamp"].timestamp()),
                # RSSI og batteri kun medtaget hvis tilgængeligt:
                **({"rs": r["rssi_dbm"]} if r.get("rssi_dbm") else {}),
                **({"bat": r["battery_pct"]} if r.get("battery_pct") else {}),
            }
            for r in readings
        ],
    }

    # MessagePack (~40% af JSON):
    packed = msgpack.packb(payload, use_bin_type=True)

    # zlib komprimering (~35% af MessagePack):
    compressed = zlib.compress(packed, level=6)

    return compressed


def deserialize_payload(data: bytes) -> dict:
    """Server-side dekomprimering."""
    decompressed = zlib.decompress(data)
    return msgpack.unpackb(decompressed, raw=False)

Heartbeat-optimering

# Undgå konstante heartbeats — send kun ved tilstandsændring:

class HeartbeatManager:
    def __init__(self, client: mqtt.Client, gateway_id: str):
        self._last_status = None
        self._client = client
        self._gateway_id = gateway_id

    def send_if_changed(self, online: bool, signal_dbm: int):
        current_status = {
            "online": online,
            "sig": signal_dbm // 10,  # Afrund til 10 dBm-bucket
        }

        if current_status != self._last_status:
            # Status ændret → send update:
            payload = msgpack.packb({
                "gw": self._gateway_id,
                "ts": int(time.time()),
                **current_status,
            })
            self._client.publish(
                f"meters/{self._gateway_id}/status",
                payload,
                qos=1,
                retain=True,  # MQTT retain = server kender seneste status
            )
            self._last_status = current_status
        # Ellers: Send IKKE (spar data)

OTA diff-patches

# server/src/ota/patcher.py
import bsdiff4  # Binary delta patching

def create_patch(old_firmware: bytes, new_firmware: bytes) -> bytes:
    """
    Opret binær diff-patch i stedet for fuld firmware-image.
    Typisk: 4 MB → 300 KB patch (93% reduktion).
    """
    patch = bsdiff4.diff(old_firmware, new_firmware)
    compressed = zlib.compress(patch, level=9)
    return compressed


def apply_patch(current_firmware: bytes, patch: bytes) -> bytes:
    """Gateway-side: Anvend patch."""
    decompressed = zlib.decompress(patch)
    return bsdiff4.patch(current_firmware, decompressed)

Fallback: Hvad sker ved SIM-problem?

# gateway/src/connectivity/fallback.py

class ConnectivityManager:
    async def send_with_fallback(self, payload: bytes):
        """
        Forsøgsrækkefølge:
        1. 4G (SIM7080G) — primær
        2. WiFi (hvis tilgængeligt på Pi) — fallback
        3. Lokal buffer — gem til næste forbindelsesforsøg
        """
        for attempt in range(3):
            try:
                await self._send_4g(payload)
                return
            except ConnectionError:
                logger.warning(f"4G forsøg {attempt+1} fejlede")
                await asyncio.sleep(60 * (2 ** attempt))

        # WiFi fallback (sjælden):
        try:
            await self._send_wifi(payload)
            return
        except Exception:
            pass

        # Gem lokalt til næste forsøg:
        await self._buffer_locally(payload)
        logger.error("Kunne ikke sende — payload buffered")

Konklusion

1NCE IoT SIM med 500 MB/10 år er tilstrækkeligt for M-Bus Gateway med den rette data-optimering: MessagePack + zlib reducerer daglig payload til 3 KB, heartbeats sendes kun ved tilstandsændring og OTA bruger binære diff-patches. Cloudflare Tunnel er den største risikofaktor — brug det sparsomt og kun til fejlfinding.

Se Raspberry Pi gateway guide eller 4G IoT SIM guide.