· IoT· gateway· høj tilgængelighed· failover· MQTT· redundans· Raspberry Pi· SIM
IoT gateway høj tilgængelighed og failover-design
IoT gateway HA-design: redundans, automatisk failover, MQTT session persistens, dual-SIM 4G fallback, watchdog-lag, database replikering og nul-data-tab ved nedbrud.
Af M-Bus Gateway
En wM-Bus gateway i kælderen skal fungere 24/7 i 10 år. Her er designprincipperne for høj tilgængelighed.
Fejlmodeller
Mulige fejl og sandsynlighed (pr. 10-årig periode):
1. SD-kort fejl (høj sandsynlighed)
→ SD-kort er det svageste led i Raspberry Pi
→ Løsning: Samsung Pro Endurance (beregnet til konstant skriv)
→ WAL-mode SQLite: Minimere skriv (én gang dagligt)
→ Backup: Platform modtager og gemmer alle payloads permanent
2. Strømafbrydelse (middel sandsynlighed)
→ UPS på gateway anbefales i kritiske installationer
→ Hardware watchdog: Genstart når strøm kommer tilbage
→ Systemd: WantedBy=multi-user.target + Restart=always
3. 4G/SIM-fejl (middel sandsynlighed)
→ 1NCE SIM: 500MB over 10 år, dækning via 450+ operatører
→ Dual-SIM modem: Waveshare SIM7080G understøtter dual-SIM
→ Fallback: WiFi via lokalt netværk (ppp0 → wlan0 failover)
4. wmbusmeters crash (lav sandsynlighed)
→ Systemd Restart=always + RestartSec=5
→ Subprocess auto-restart i Python listener
5. Hetzner server downtime (meget lav sandsynlighed)
→ Hetzner SLA 99,9% (8,7 timer nedetid/år)
→ Gateway buffer: 7 dage × data lokalt i SQLite
→ Data sendes ved næste vellykkede forbindelse
Hardware watchdog (Lag 1)
# gateway/src/watchdog.py
import fcntl
import struct
import asyncio
import logging
WATCHDOG_DEVICE = "/dev/watchdog"
WATCHDOG_IOCTL_SETTIMEOUT = 0xC0045706
WATCHDOG_TIMEOUT = 15 # sekunder
class HardwareWatchdog:
def __init__(self):
self._fd = None
def open(self) -> None:
self._fd = open(WATCHDOG_DEVICE, "wb", buffering=0)
# Sæt timeout til 15 sekunder
fcntl.ioctl(
self._fd,
WATCHDOG_IOCTL_SETTIMEOUT,
struct.pack("I", WATCHDOG_TIMEOUT),
)
def feed(self) -> None:
"""Skriv til watchdog — forhindrer reboot."""
if self._fd:
self._fd.write(b"\x01")
def close(self) -> None:
"""Skriv 'V' for at deaktivere watchdog-reboot ved kontrolleret stop."""
if self._fd:
self._fd.write(b"V")
self._fd.close()
async def watchdog_loop(dog: HardwareWatchdog) -> None:
"""Feed hardware watchdog hvert 10. sekund."""
while True:
dog.feed()
await asyncio.sleep(10)
Systemd watchdog (Lag 2)
# /etc/systemd/system/mbus-gateway.service
[Unit]
Description=M-Bus Gateway
After=network-online.target mosquitto.service
Wants=network-online.target
BindsTo=dev-wmbus.device
[Service]
Type=notify
User=mbus
WorkingDirectory=/opt/mbus-gateway
ExecStart=/opt/mbus-gateway/.venv/bin/python -m gateway.src.main
Restart=always
RestartSec=5
WatchdogSec=120
NotifyAccess=main
# Ressource-begrænsning
MemoryMax=256M
CPUQuota=80%
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mbus-gateway
[Install]
WantedBy=multi-user.target
# Systemd watchdog notification:
import sdnotify
notifier = sdnotify.SystemdNotifier()
notifier.notify("READY=1") # Systemd: service er klar
# I hoved-loop:
async def main():
while True:
await do_work()
notifier.notify("WATCHDOG=1") # Reset systemd watchdog timer
await asyncio.sleep(60)
4G/WiFi failover
# gateway/src/cellular/failover.py
import asyncio
import subprocess
from enum import Enum
class ConnectivityMode(Enum):
CELLULAR = "ppp0"
WIFI = "wlan0"
ETHERNET = "eth0"
async def get_active_interface() -> ConnectivityMode:
"""Tjek hvilken interface der har internet-forbindelse."""
for mode in ConnectivityMode:
try:
result = await asyncio.create_subprocess_exec(
"ping", "-I", mode.value, "-c", "1", "-W", "3",
"8.8.8.8",
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await result.wait()
if result.returncode == 0:
return mode
except Exception:
continue
raise RuntimeError("Ingen internet-forbindelse tilgængelig")
async def ensure_connectivity() -> str:
"""Returnér aktiv interface — prøver 4G, WiFi, Ethernet."""
mode = await get_active_interface()
return mode.value
MQTT session persistens
# gateway/src/mqtt/client.py
import paho.mqtt.client as mqtt
def create_mqtt_client(gateway_id: str) -> mqtt.Client:
"""
clean_session=False: Broker husker subscription og queued messages
ved genforbindelse — ingen data mistes under kortvarig nedetid.
"""
client = mqtt.Client(
client_id=f"gw-{gateway_id}",
clean_session=False, # Persistent session
protocol=mqtt.MQTTv311,
)
# Last Will: Broker sender dette ved uventet disconnect
client.will_set(
topic=f"meters/{gateway_id}/status",
payload='{"online": false, "reason": "unexpected_disconnect"}',
qos=1,
retain=True,
)
client.reconnect_delay_set(min_delay=1, max_delay=120)
return client
Data-buffer og nul-tab design
Gateway data-flow (nul-tab garanti):
1. wmbusmeters → RAM buffer (hele dagen)
→ Data mistes kun ved strømafbrydelse i løbet af dagen
→ Risiko: Op til 23:59 timeers data (acceptabelt — HCA-data er kumulativ)
2. kl. 06:00 UTC → SQLite WAL (commit til disk)
→ Data persisteres inden MQTT-send
→ SQLite WAL: Atomic writes — ingen korrupte filer
3. MQTT send → Hetzner
→ QoS 1: Mindst én levering garanteret
→ Retry ved fejl: Eksponentiel backoff (1s, 2s, 4s, 8s, max 5 min)
→ Timeout: 30 sekunder pr. forsøg
4. ACK modtaget → SQLite: Mark as "sent"
→ Resend logik: Finder "unsent" records ved næste kl. 06:00
→ Buffer: 7 dage (168 forsøg inden data betragtes som tabt)
5. Hetzner server → TimescaleDB
→ Duplikat-beskyttelse: ON CONFLICT DO UPDATE (upsert)
→ Idempotent: Gensend af samme data er ufarlig
Recovery ved SD-kort fejl
# Bootstrap ny Pi fra scratch (ny SD-kort):
# 1. Flash Raspberry Pi OS Lite 64-bit på ny SD-kort
# 2. Boot Pi → SSH ind
# 3. Kør bootstrap-script fra Hetzner:
curl http://178.105.90.8:8765/bootstrap.sh | sudo bash
# Bootstrap-script:
# → Installerer wmbusmeters fra GitHub releases
# → Henter gateway-konfiguration fra Hetzner (gateway_id + MQTT creds)
# → Starter systemd service
# → Pi er operationel inden for 15 minutter
# Data: Ingenting mistes
# → Alle HCA-aflæsninger er gemt på Hetzner TimescaleDB
# → SQLite på Pi er blot en dagsbuffer
Monitoring og alerting
Platform overvåger:
→ "Gateway offline": Ingen heartbeat i 36+ timer → email til udlejer
→ "Payload missing": Ingen daglig data i 48+ timer → alarm
→ "RSSI degraded": Signal fald → teknikernotifikation
→ "Battery low": Under 20% → advarselsmail
Hetzner server monitoring:
→ Uptime Robot: HTTP check hvert 5. minut
→ Grafana Loki: Log-analyse, alerting ved ERROR-rate
→ TimescaleDB: Antal readings pr. gateway pr. dag (fald = alarm)
Konklusion
Gateway HA-design handler om lag-vis redundans: hardware watchdog til Pi-reboot, systemd til service-genstart, MQTT persistent session til dataintegritet, og 7-dages SQLite buffer til server-downtime. Recovery fra SD-kort fejl sker på under 15 minutter via bootstrap-script — ingen data mistes da alle aflæsninger er persisterede på Hetzner.