"""Eigener Lauf des Hausmelders gegen einen mitschreibenden Server auf 127.0.0.1 -- und wahlweise ein POST ans echte HA. Koordinator-Pruefcode, nicht Teil des Repos. Teil 1 (immer): echter Hausmelder-Faden, echter requests-POST, lokaler HTTP-Server, gestellte Statusdatei, Wegwerf-Register. Teil 2 (nur mit --echt ): ein einziger pruefe()-Durchlauf mit unterscheidbaren Werten an die Adresse aus der Geheimnisdatei; die Adresse wird nie ausgegeben. Rueckgelesen wird danach unter Windows mit ha_einrichten.py. Aufruf (WSL): python hausmelder_echtlauf.py [--echt ] """ import json import os import sys import threading import time from datetime import UTC from datetime import datetime from http.server import BaseHTTPRequestHandler from http.server import ThreadingHTTPServer from pathlib import Path CODE = Path(sys.argv[1]).resolve() WEG = Path(sys.argv[2]).resolve() ECHT_SECRETS = Path(sys.argv[4]).resolve() if len(sys.argv) > 4 and sys.argv[3] == "--echt" else None os.chdir(CODE) sys.path.insert(0, str(CODE)) from trading.dashboard import hausmelder as modul # noqa: E402 from trading.dashboard.hausmelder import Hausmelder # noqa: E402 from trading.dashboard.hausmelder import killswitch_leser # noqa: E402 from trading.dashboard.hausmelder import lies_hausmelder_config # noqa: E402 from trading.dashboard.hausmelder import starte_hausmelder # noqa: E402 from trading.paper.status import schreibe_status # noqa: E402 from trading.registry.registry import Registry # noqa: E402 EMPFANGEN = [] ANTWORT = {"code": 200, "location": None} class Mitschreiber(BaseHTTPRequestHandler): def do_POST(self): laenge = int(self.headers.get("Content-Length", "0")) rumpf = self.rfile.read(laenge) EMPFANGEN.append({"pfad": self.path, "typ": self.headers.get("Content-Type"), "daten": json.loads(rumpf.decode("utf-8")), "antwort": ANTWORT["code"]}) self.send_response(ANTWORT["code"]) if ANTWORT["location"]: self.send_header("Location", ANTWORT["location"]) self.send_header("Content-Length", "0") self.end_headers() def do_GET(self): EMPFANGEN.append({"pfad": self.path, "typ": "GET", "daten": None, "antwort": 200}) self.send_response(200) self.send_header("Content-Length", "0") self.end_headers() def log_message(self, *a): pass def status(pfad, **felder): grund = {"zeit": datetime.now(UTC).isoformat(), "modus": "paper", "phase": "handelt", "position": 0, "kontowert": 100000.0, "startkapital": 100000.0} grund.update(felder) schreibe_status(pfad, grund) def warte_bis(bedingung, sekunden=5.0): ende = time.monotonic() + sekunden while time.monotonic() < ende: if bedingung(): return True time.sleep(0.05) return False def main(): WEG.mkdir(parents=True, exist_ok=False) befunde = [] server = ThreadingHTTPServer(("127.0.0.1", 0), Mitschreiber) port = server.server_address[1] threading.Thread(target=server.serve_forever, daemon=True).start() kennung = "nachtlauf-" + "a" * 40 url = f"http://127.0.0.1:{port}/api/webhook/{kennung}" registry = Registry(db_pfad=WEG / "registry.db", artefakt_wurzel=WEG / "artifacts") registry.setze_killswitch_zustand("angehalten", ausloeser="neustart") statusdatei = WEG / "paper" / "status.json" statusdatei.parent.mkdir(parents=True) # Teil 1a: der Faden selbst, mit kurzem Takt. melder = Hausmelder(webhook_url=url, status_datei=statusdatei, killswitch=killswitch_leser(registry), takt_s=0.1, lebenszeichen_s=1.5) melder.starte() if not warte_bis(lambda: len(EMPFANGEN) == 1): befunde.append("kein erster POST ohne Statusdatei") erster = EMPFANGEN[0]["daten"] if EMPFANGEN else {} print("1. ohne Statusdatei:", json.dumps(erster, ensure_ascii=False)) if erster.get("phase") != "kein_status" or erster.get("killswitch") != "angehalten" or erster.get("herzschlag_alter_s") != -1: befunde.append("Rueckfall ohne Statusdatei stimmt nicht") if EMPFANGEN and (EMPFANGEN[0]["pfad"] != f"/api/webhook/{kennung}" or EMPFANGEN[0]["typ"] != "application/json"): befunde.append("Pfad oder Content-Type falsch") status(statusdatei, phase="handelt", position=20, kontowert=100012.345) warte_bis(lambda: len(EMPFANGEN) == 2) zweiter = EMPFANGEN[-1]["daten"] print("2. nach Statusaenderung:", json.dumps(zweiter, ensure_ascii=False)) if zweiter.get("position") != 20 or zweiter.get("tagesergebnis_usd") != 12.35 or zweiter.get("phase") != "handelt": befunde.append("Statuswerte kommen nicht an") # Ohne Aenderung: In 0,8 s (acht Takte) darf nichts kommen; der Herzschlag wird dabei aelter. stand = len(EMPFANGEN) time.sleep(0.8) if len(EMPFANGEN) != stand: befunde.append(f"ohne Aenderung kamen {len(EMPFANGEN) - stand} POST(s)") # Lebenszeichen nach 1,5 s. if not warte_bis(lambda: len(EMPFANGEN) == stand + 1, 3.0): befunde.append("kein Lebenszeichen") print(f"3. ohne Aenderung still, Lebenszeichen nach der Frist: {len(EMPFANGEN)} POSTs bisher") # Killswitch aus dem Register, ohne dass die Statusdatei sich aendert. registry.setze_killswitch_zustand("gesperrt", ausloeser="pascal") stand = len(EMPFANGEN) warte_bis(lambda: len(EMPFANGEN) == stand + 1) print("4. Killswitch:", EMPFANGEN[-1]["daten"].get("killswitch")) if EMPFANGEN[-1]["daten"].get("killswitch") != "gesperrt": befunde.append("Killswitch-Wechsel kommt nicht an") # Fehlschlag: 500 kippt den Merker nicht, es wird wiederholt; danach 200. ANTWORT["code"] = 500 status(statusdatei, phase="pausiert", position=0, kontowert=99990.0) stand = len(EMPFANGEN) warte_bis(lambda: len(EMPFANGEN) >= stand + 3) wiederholt = len(EMPFANGEN) - stand ANTWORT["code"] = 200 warte_bis(lambda: EMPFANGEN[-1]["antwort"] == 200) stand = len(EMPFANGEN) time.sleep(0.6) print(f"5. bei HTTP 500 wiederholt: {wiederholt} Versuche; nach 200 still: {len(EMPFANGEN) - stand} weitere; " f"Fehlschlaege: {sorted(set(melder.fehlschlaege))}") if wiederholt < 3 or len(EMPFANGEN) != stand: befunde.append("Wiederholung nach Fehlschlag stimmt nicht") if EMPFANGEN[-1]["daten"].get("tagesergebnis_usd") != -10.0: befunde.append("negatives Tagesergebnis stimmt nicht") # Weiterleitung: wird nicht gefolgt (kein GET am Server), gilt als Fehlschlag. ANTWORT["code"] = 302 ANTWORT["location"] = f"http://127.0.0.1:{port}/woanders" status(statusdatei, phase="handelt", position=10) stand = len(EMPFANGEN) warte_bis(lambda: len(EMPFANGEN) >= stand + 2) gets = [e for e in EMPFANGEN if e["typ"] == "GET" or e["pfad"] == "/woanders"] ANTWORT["code"] = 200 ANTWORT["location"] = None warte_bis(lambda: EMPFANGEN[-1]["antwort"] == 200) print(f"6. Weiterleitung: gefolgt {len(gets)}-mal; Fehlschlaege: {sorted(set(melder.fehlschlaege))}") if gets: befunde.append("einer Weiterleitung wurde gefolgt") # Veraltet: Herzschlag 200 s alt, Phase nicht beendet. status(statusdatei, phase="handelt", position=10, zeit=datetime.fromtimestamp(time.time() - 200, UTC).isoformat()) stand = len(EMPFANGEN) warte_bis(lambda: len(EMPFANGEN) >= stand + 1) print("7. veraltet:", EMPFANGEN[-1]["daten"].get("veraltet"), EMPFANGEN[-1]["daten"].get("herzschlag_alter_s")) if EMPFANGEN[-1]["daten"].get("veraltet") is not True: befunde.append("veraltet wird nicht gemeldet") melder.stoppe() if melder.laeuft: befunde.append("Faden laeuft nach stoppe() weiter") geheim = [text for text in melder.fehlschlaege if kennung in text or "127.0.0.1" in text] if geheim or kennung in repr(melder): befunde.append("Adresse in Fehlschlag oder repr") schluessel = sorted(EMPFANGEN[-1]["daten"]) print("8. Schluessel der Nutzlast:", schluessel) if schluessel != sorted(["phase", "modus", "killswitch", "position", "tagesergebnis_usd", "herzschlag_alter_s", "veraltet", "gesendet"]): befunde.append("Nutzlast traegt andere Schluessel") # Teil 1b: Verdrahtung ueber die Geheimnisdatei. secrets = WEG / "secrets.toml" secrets.write_text(f'[homeassistant]\nwebhook_url = "{url}"\naktiv = true\n', encoding="utf-8") stand = len(EMPFANGEN) zweiter_melder = starte_hausmelder(status_datei=statusdatei, killswitch=killswitch_leser(registry), secrets_pfad=secrets) ok = zweiter_melder is not None and warte_bis(lambda: len(EMPFANGEN) > stand) if zweiter_melder is not None: zweiter_melder.stoppe() print(f"9. starte_hausmelder mit aktiv = true: Faden {'lief und hat gesendet' if ok else 'FEHLT'}") if not ok: befunde.append("starte_hausmelder sendet nicht") secrets.write_text(f'[homeassistant]\nwebhook_url = "{url}"\naktiv = false\n', encoding="utf-8") if starte_hausmelder(status_datei=statusdatei, killswitch=killswitch_leser(registry), secrets_pfad=secrets) is not None: befunde.append("aktiv = false startet einen Faden") secrets.write_text('[homeassistant]\nwebhook_url = "ftp://falsch"\naktiv = true\n', encoding="utf-8") if starte_hausmelder(status_datei=statusdatei, killswitch=killswitch_leser(registry), secrets_pfad=secrets) is not None: befunde.append("eine falsche Adresse startet einen Faden") server.shutdown() # Teil 2: ein POST ans echte HA, mit Werten, die sich von der neutralen Lage unterscheiden. if ECHT_SECRETS is not None: config = lies_hausmelder_config(ECHT_SECRETS) if config is None or not config.webhook_url: befunde.append("echte Geheimnisdatei ohne [homeassistant]") else: status(statusdatei, modus="generalprobe", phase="handelt", position=7, kontowert=100004.56) registry.setze_killswitch_zustand("aktiv", ausloeser="pascal") echter = Hausmelder(webhook_url=config.webhook_url, status_datei=statusdatei, killswitch=killswitch_leser(registry)) echter.pruefe() print(f"10. echtes HA: zugestellt={echter._zuletzt is not None}, Fehlschlaege={echter.fehlschlaege}, " f"Laenge der Adresse {len(config.webhook_url)}") if echter._zuletzt is None: befunde.append("echtes HA hat nicht mit 200 geantwortet") print(f"{len(befunde)} Befund(e): {befunde}") return 2 if befunde else 0 if __name__ == "__main__": sys.exit(main())