Hi Peter
Sorry it took so long. I’m currently in the rainforest in Malaysia, almost no internet… well, here’s the script:
#!/usr/bin/env python3
import socket
import csv
import json
import meterbus
from io import StringIO
from datetime import datetime
from flask import Flask, make_response
HOST = "192.168.1.150" # TRB143 IP
PORT = 5020 # TRB143 M-Bus TCP Port
SCAN_START = 1
SCAN_END = 10
META_KEYS = ["addr", "manufacturer", "identification", "medium", "version", "access_no"]
app = Flask(__name__)
def build_req_ud2_short(addr: int) -> bytes:
C = 0x5B
checksum = (C + addr) & 0xFF
return bytes([0x10, C, addr, checksum, 0x16])
def build_req_ud1_short(addr: int) -> bytes:
C = 0x5A # REQ_UD1 (Class 1 data)
checksum = (C + addr) & 0xFF
return bytes([0x10, C, addr, checksum, 0x16])
def recv_mbus_frame(sock: socket.socket) -> bytes:
first = sock.recv(1)
if not first:
raise IOError("No data")
if first[0] == 0x10: # Short frame
rest = sock.recv(4)
if len(rest) < 4:
raise IOError("Incomplete short frame")
return first + rest
if first[0] != 0x68:
raise IOError(f"Unexpected start byte: {first[0]:02x}")
header = sock.recv(3)
if len(header) < 3:
raise IOError("Incomplete long frame header")
length = header[0]
body = bytearray()
while len(body) < length + 2:
chunk = sock.recv(length + 2 - len(body))
if not chunk:
raise IOError("Connection closed while reading body")
body.extend(chunk)
return bytes(b"\x68" + header + body)
def ping_meter(addr: int, req_builder=build_req_ud2_short, timeout: float = 3.0) -> bytes | None:
try:
with socket.create_connection((HOST, PORT), timeout=timeout) as s:
req = req_builder(addr)
s.sendall(req)
raw = recv_mbus_frame(s)
return raw
except Exception:
return None
def normalize_key(r):
type_str = r.get("type") or ""
type_short = type_str.split(".")[-1] if "." in type_str else type_str
return f"{type_short}_S0"
def record_priority(r):
"""
Kleinere Zahl = bessere Priorität
"""
if r["storage_number"] == 0:
return 0
if "INSTANT" in r["function"] or "CURRENT" in r["function"]:
return 1
return 2
def read_meter(addr: int):
# 1) Class‑2 Daten (REQ_UD2)
raw_ud2 = ping_meter(addr, req_builder=build_req_ud2_short)
if not raw_ud2:
return None
t2 = meterbus.load(raw_ud2)
data2 = json.loads(t2.to_JSON())
header = data2["body"]["header"]
records_json = list(data2["body"]["records"])
# 2) Optional: Class‑1 Daten (REQ_UD1, Alarm/Status)
raw_ud1 = ping_meter(addr, req_builder=build_req_ud1_short)
if raw_ud1:
t1 = meterbus.load(raw_ud1)
data1 = json.loads(t1.to_JSON())
records_json.extend(data1["body"]["records"])
selected = {}
for r in records_json:
if r["value"] is None:
continue
if r["function"] == "FunctionType.MORE_RECORDS_FOLLOW":
continue
key = normalize_key(r)
prio = record_priority(r)
current = selected.get(key)
if current is None or prio < current["_prio"]:
rec = {
"key": key,
"value": r["value"],
"unit": r["unit"],
"storage_number": r["storage_number"],
"function": r["function"],
"_prio": prio,
}
if key == "ENERGY_WH_S0":
rec["derived"] = r["value"] / 1_000_000
rec["derived_unit"] = "MWh"
else:
rec["derived"] = None
rec["derived_unit"] = None
selected[key] = rec
records = list(selected.values())
for r in records:
r.pop("_prio", None)
return {
"addr": addr,
"manufacturer": header["manufacturer"],
"identification": header["identification"],
"medium": header["medium"],
"version": header["version"],
"access_no": header["access_no"],
"records": records,
}
def scan_meters():
meters = {}
for addr in range(SCAN_START, SCAN_END + 1):
m = read_meter(addr)
if m:
print(f"Found meter at {addr}: {m['manufacturer']} {m['identification']}")
meters[addr] = m
return meters
def build_pivot(meters: dict):
meter_addrs = sorted(meters.keys())
# 1) Metadaten-Zeilen
meta_rows = []
for meta_key in META_KEYS:
row = {"param": meta_key.upper(), "values": {}}
for addr in meter_addrs:
m = meters[addr]
row["values"][addr] = m.get(meta_key, "")
meta_rows.append(row)
# 2) Normale Werte
param_keys = sorted({
rec["key"]
for m in meters.values()
for rec in m["records"]
})
data_rows = []
for key in param_keys:
row = {"param": key, "values": {}}
for addr in meter_addrs:
m = meters[addr]
rec = next((r for r in m["records"] if r["key"] == key), None)
row["values"][addr] = rec["value"] if rec else ""
data_rows.append(row)
# 3) Abgeleitete Werte (z.B. *_DERIVED)
derived_rows = []
derived_keys = sorted({
rec["key"] + "_DERIVED"
for m in meters.values()
for rec in m["records"]
if rec["derived"] is not None
})
for key in derived_keys:
base_key = key.replace("_DERIVED", "")
row = {"param": key, "values": {}}
for addr in meter_addrs:
m = meters[addr]
rec = next(
(r for r in m["records"]
if r["key"] == base_key and r["derived"] is not None),
None
)
row["values"][addr] = rec["derived"] if rec else ""
derived_rows.append(row)
# 4) Gesamtliste flach zurückgeben
all_rows = meta_rows + data_rows + derived_rows
return meter_addrs, all_rows
@app.route("/")
def index():
meters = scan_meters()
if not meters:
return "<h1>No meters found</h1>"
meter_addrs, rows = build_pivot(meters)
html = []
html.append("<!doctype html><html lang='de'><head>")
html.append("<meta charset='utf-8'><title>M-Bus Meter</title>")
html.append("<style>table{border-collapse:collapse;margin-bottom:1rem}"
"th,td{border:1px solid #ccc;padding:4px 8px}th{background:#eee}</style>")
html.append("</head><body>")
html.append("<h1>M-Bus Meter</h1>")
html.append("<p><a href='/export'>CSV exportieren</a></p>")
html.append("<h2>Parameter-Tabelle</h2>")
html.append("<table><tr><th>param</th>")
for addr in meter_addrs:
html.append(f"<th>meter_{addr}</th>")
html.append("</tr>")
for row in rows:
html.append(f"<tr><td>{row['param']}</td>")
for addr in meter_addrs:
html.append(f"<td>{row['values'][addr]}</td>")
html.append("</tr>")
html.append("</table>")
html.append("</body></html>")
return "".join(html)
@app.route("/export")
def export_csv():
meters = scan_meters()
if not meters:
resp = make_response("No meters found.")
resp.headers["Content-Type"] = "text/plain"
return resp
meter_addrs, rows = build_pivot(meters)
si = StringIO()
writer = csv.writer(si)
header = ["param"] + [f"meter_{addr}" for addr in meter_addrs]
writer.writerow(header)
for row in rows:
writer.writerow(
[row["param"]] + [row["values"][addr] for addr in meter_addrs]
)
csv_data = si.getvalue()
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
resp = make_response(csv_data)
resp.headers["Content-Disposition"] = f"attachment; filename=meters-{ts}.csv"
resp.headers["Content-Type"] = "text/csv"
return resp
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5021, debug=True)