Files

162 lines
6.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
heat: Observatoř Wireless Heating Telemetry CLI
Manages, queries, and inspects 868.95 MHz Wireless M-Bus radiator metrics.
"""
import os
import sys
import json
import sqlite3
import subprocess
from datetime import datetime
HOME = os.path.expanduser("~")
DATA_DIR = os.path.join(HOME, "src", "observator", "data")
DB_PATH = os.path.join(DATA_DIR, "heat_meters.db")
JSONL_PATH = os.path.join(DATA_DIR, "heat_meters.jsonl")
SHM_LATEST = "/dev/shm/latest_heat_meter.json"
SHM_SUMMARY = "/dev/shm/heat_fleet_summary.json"
SERVICE_NAME = "sdr-heat.service"
def cmd_status():
if not os.path.exists(DB_PATH):
print("[-] Žádná databáze měřičů topení zatím neexistuje.")
print(" Spusťte sběrač pomocí: heat start")
return
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
SELECT id, manufacturer, room_label, last_seen, last_hca, prev_hca, billing_date, total_packets
FROM meters ORDER BY last_seen DESC
""")
rows = cur.fetchall()
conn.close()
print("╔═══════════════════════════════════════════════════════════════════════════════════════════════════════╗")
print("║ THE OBSERVATOŘ // RADIÁTORY & INDIKÁTORY TOPENÍ (868.95 MHz Wireless M-Bus Mode C&T) ║")
print("╚═══════════════════════════════════════════════════════════════════════════════════════════════════════╝")
# Check daemon status
is_active = subprocess.run(["systemctl", "--user", "is-active", "--quiet", SERVICE_NAME]).returncode == 0
status_str = "\033[32m● AKTIVNÍ (démon běží)\033[0m" if is_active else "\033[31m○ NEAKTIVNÍ\033[0m"
print(f" Služba sdr-heat: {status_str} | Tuner: RTL-SDR Fitipower FC0013 (t520) | Zisk: 19.7 dB")
print("─" * 105)
if not rows:
print(" [!] V databázi zatím není zaznamenán žádný indikátor. Vyčkejte na vysílací interval.")
return
header = f" {'ID MĚŘIČE':<12} │ {'VÝROBCE':<8} │ {'MÍSTNOST / ŠTÍTEK':<26} │ {'DÍLKY':>8} │ {'MINULE':>8} │ {'POSLEDNÍ SIGNÁL':<19} │ {'PKT':>5}"
print(header)
print("═" * 105)
total_units = 0.0
for r in rows:
mid, mfg, label, last_seen, hca, prev_hca, billing_date, pkts = r
total_units += (hca or 0.0)
lbl = label if label else "—"
hca_str = f"{hca:.1f}" if hca is not None else "0.0"
prev_str = f"{prev_hca:.1f}" if prev_hca is not None else "0.0"
print(f" {mid:<12} │ {mfg:<8} │ {lbl:<26} │ {hca_str:>8} │ {prev_str:>8} │ {last_seen:<19} │ {pkts:>5}")
print("─" * 105)
print(f" Celkem indikátorů: {len(rows)} │ Celková naměřená spotřeba: {total_units:.1f} dílků")
print(" Tip: Pro pojmenování radiátoru použijte: heat label <ID> \"Kuchyň okno\"")
print()
def cmd_label(args):
if len(args) < 2:
print("Použití: heat label <ID_MĚŘIČE> <NÁZEV MÍSTNOSTI>")
print("Příklad: heat label 18466419 \"Kuchyň velký radiátor\"")
return
meter_id = args[0]
label = " ".join(args[1:])
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("UPDATE meters SET room_label = ? WHERE id = ?", (label, meter_id))
if cur.rowcount > 0:
conn.commit()
print(f"[✓] Indikátor #{meter_id} úspěšně označen jako: '{label}'")
else:
# Insert placeholder if not seen yet
now_ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cur.execute("""
INSERT INTO meters (id, manufacturer, model, room_label, first_seen, last_seen, last_hca, prev_hca, billing_date, total_packets)
VALUES (?, 'UNK', 'HCA', ?, ?, ?, 0.0, 0.0, '', 0)
""", (meter_id, label, now_ts, now_ts))
conn.commit()
print(f"[✓] Vytvořen nový záznam pro #{meter_id} s označením: '{label}'")
conn.close()
def cmd_live():
print("[*] Spouštím živý odposlech paketů radiátorů na 868.95 MHz (Ctrl+C pro ukončení)...")
cmd = ["rtl_433", "-f", "868.95M", "-s", "1200k", "-R", "104", "-g", "19.7", "-F", "json"]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
try:
for line in proc.stdout:
line = line.strip()
if not line:
continue
try:
pkt = json.loads(line)
mid = pkt.get("id", "UNK")
mfg = pkt.get("M", "UNK")
hca = pkt.get("inst_hca_0", "0.0").strip()
t_str = pkt.get("time", "")
data_hex = pkt.get("data", "")
print(f"[{t_str}] \033[36m#{mid}\033[0m ({mfg}) ➔ dílky: \033[32m{hca}\033[0m │ raw: {data_hex[:24]}...")
except json.JSONDecodeError:
pass
except KeyboardInterrupt:
proc.terminate()
print("\n[*] Živý odposlech ukončen.")
def cmd_log(n=10):
if not os.path.exists(JSONL_PATH):
print("[-] Žádný archiv paketů zatím neexistuje.")
return
cmd = ["tail", "-n", str(n), JSONL_PATH]
subprocess.run(cmd)
def cmd_service(action):
if action in ["start", "stop", "restart", "status", "enable", "disable"]:
subprocess.run(["systemctl", "--user", action, SERVICE_NAME])
else:
print(f"Neznámá akce služby: {action}")
def main():
args = sys.argv[1:]
if not args or args[0] in ["status", "s"]:
cmd_status()
elif args[0] in ["label", "l"]:
cmd_label(args[1:])
elif args[0] in ["live", "watch", "w"]:
cmd_live()
elif args[0] in ["log", "tail"]:
n = int(args[1]) if len(args) > 1 else 10
cmd_log(n)
elif args[0] in ["json", "summary"]:
if os.path.exists(SHM_SUMMARY):
with open(SHM_SUMMARY) as f:
print(f.read())
else:
print("{}")
elif args[0] in ["start", "stop", "restart", "enable", "disable"]:
cmd_service(args[0])
else:
print("Observatoř Heat Telemetry CLI")
print("Použití:")
print(" heat status Zobrazí přehled všech zachycených radiátorů")
print(" heat live Živý streaming příchozích paketů v terminálu")
print(" heat label <ID> <NÁZEV> Pojmenuje radiátor (např. 'Kuchyň')")
print(" heat log [N] Vypíše posledních N surových JSON paketů")
print(" heat json Vypíše agregovaný JSON z /dev/shm")
print(" heat start|stop|restart Ovládání systemd služby")
if __name__ == "__main__":
main()