Automatischer Push: 14 Datei(en) geändert
- .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/dienste_stand.sh - .vault-tools/trading-plattform-pruefung/ha_einrichten.py - .vault-tools/trading-plattform-pruefung/hole_kerzen.py - .vault-tools/trading-plattform-pruefung/neustart_echtlauf.py - .vault-tools/trading-plattform-pruefung/ntfy_lesen.py - .vault-tools/trading-plattform-pruefung/pause_echtlauf.py - .vault-tools/trading-plattform-pruefung/wiederanlauf_probe.ps1 - AI/Regelwerk/codeworkflow.md - AI/Regelwerk/projekte.md - … und 2 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
395642c480
commit
246af19683
14 files changed
+964
-7
No files matched your search
@@ -0,0 +1,231 @@
|
||||
"""Richtet in Home Assistant die Gegenstelle fuer die Kennzahlen der Trading Plattform ein.
|
||||
|
||||
Koordinator-Werkzeug, nicht Teil des Repos. Laeuft unter WINDOWS mit dem Python des HA-Projekts
|
||||
(dort liegen `websockets` und `src/ha_verbindung.py`; Adresse der Instanz und der Name der
|
||||
Token-Variable stehen in jenem Modul, nicht hier). Das HA-Token bleibt auf der Windows-Seite und
|
||||
wird nie ausgegeben; auf den Trading-Rechner kommt nur die Webhook-Adresse.
|
||||
|
||||
Aktionen:
|
||||
pruefen nur lesend: Version, vorhandene Helfer mit dem Praefix, vorhandene Automation
|
||||
lesen nur lesend: wie pruefen, dazu die Zustaende der Helfer, wie sie gerade in Home Assistant stehen
|
||||
einrichten legt acht Helfer und eine Automation mit Webhook-Ausloeser an (local_only, nur POST),
|
||||
erzeugt dafuer eine Webhook-ID (64 Hex-Zeichen) und traegt die Adresse in die
|
||||
config/secrets.toml des Trading-Projekts ein ([homeassistant], aktiv = false).
|
||||
Gibt die ID nie aus. Bricht ab, wenn es den Abschnitt oder die Automation schon gibt.
|
||||
probe schickt eine Beispiel-Nutzlast an den Webhook (Adresse aus der Geheimnisdatei) und
|
||||
liest die Helfer ueber die HA-API zurueck
|
||||
entfernen loescht Automation und Helfer wieder; der Abschnitt in der Geheimnisdatei bleibt stehen
|
||||
und wird genannt
|
||||
|
||||
Aufruf: <ha-projekt>\\.venv\\Scripts\\python.exe ha_einrichten.py <aktion> <ha-projektordner> <trading-projektordner>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
AUTOMATION_ID = "trading_plattform_kennzahlen"
|
||||
ALIAS = "Trading Plattform: Kennzahlen uebernehmen"
|
||||
PRAEFIX = "trading_"
|
||||
|
||||
TEXTE = ("Trading Phase", "Trading Modus", "Trading Killswitch")
|
||||
ZAHLEN = (
|
||||
("Trading Position", -100000, 100000, 1, "Stk"),
|
||||
("Trading Tagesergebnis USD", -1000000, 1000000, 0.01, "USD"),
|
||||
("Trading Herzschlag Alter s", -1, 100000000, 1, "s"),
|
||||
)
|
||||
SCHALTER = ("Trading Veraltet",)
|
||||
ZEITEN = ("Trading Gesendet",)
|
||||
|
||||
|
||||
def aktionen() -> list[dict]:
|
||||
def text(ziel: str, schluessel: str, vorgabe: str) -> dict:
|
||||
return {
|
||||
"service": "input_text.set_value",
|
||||
"target": {"entity_id": f"input_text.{ziel}"},
|
||||
"data": {"value": "{{ (trigger.json." + schluessel + " | default('" + vorgabe + "') | string)[:100] }}"},
|
||||
"continue_on_error": True,
|
||||
}
|
||||
|
||||
def zahl(ziel: str, schluessel: str, filter_: str) -> dict:
|
||||
return {
|
||||
"service": "input_number.set_value",
|
||||
"target": {"entity_id": f"input_number.{ziel}"},
|
||||
"data": {"value": "{{ trigger.json." + schluessel + " | default(0) | " + filter_ + " }}"},
|
||||
"continue_on_error": True,
|
||||
}
|
||||
|
||||
return [
|
||||
text("trading_phase", "phase", "unbekannt"),
|
||||
text("trading_modus", "modus", ""),
|
||||
text("trading_killswitch", "killswitch", "unbekannt"),
|
||||
zahl("trading_position", "position", "int(0)"),
|
||||
zahl("trading_tagesergebnis_usd", "tagesergebnis_usd", "float(0) | round(2)"),
|
||||
zahl("trading_herzschlag_alter_s", "herzschlag_alter_s", "int(-1)"),
|
||||
{
|
||||
"service": "input_boolean.turn_{{ 'on' if (trigger.json.veraltet | default(false)) else 'off' }}",
|
||||
"target": {"entity_id": "input_boolean.trading_veraltet"},
|
||||
"continue_on_error": True,
|
||||
},
|
||||
{
|
||||
"service": "input_datetime.set_datetime",
|
||||
"target": {"entity_id": "input_datetime.trading_gesendet"},
|
||||
"data": {"timestamp": "{{ now().timestamp() }}"},
|
||||
"continue_on_error": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def lies_abschnitt(datei: Path) -> dict | None:
|
||||
return tomllib.loads(datei.read_text(encoding="utf-8")).get("homeassistant")
|
||||
|
||||
|
||||
def trage_ein(datei: Path, adresse: str) -> None:
|
||||
roh = datei.read_text(encoding="utf-8")
|
||||
vorher = tomllib.loads(roh)
|
||||
block = (
|
||||
"\n[homeassistant]\n"
|
||||
"# Webhook in Home Assistant fuer die Kennzahlen des Paper-Runners. Die ID in der Adresse ist das\n"
|
||||
"# Geheimnis: Wer sie kennt, kann die Anzeige in HA faelschen. Eingeschaltet wird mit aktiv = true.\n"
|
||||
f'webhook_url = "{adresse}"\n'
|
||||
"aktiv = false\n"
|
||||
)
|
||||
neu = roh.rstrip("\n") + "\n" + block
|
||||
nachher = tomllib.loads(neu)
|
||||
if {k: v for k, v in nachher.items() if k != "homeassistant"} != vorher:
|
||||
raise SystemExit("ABBRUCH: bestehende Abschnitte haetten sich geaendert")
|
||||
nachbar = datei.with_name(datei.name + ".neu")
|
||||
with open(nachbar, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(neu)
|
||||
os.replace(nachbar, datei)
|
||||
|
||||
|
||||
def helfer_liste(ha) -> dict[str, list[dict]]:
|
||||
ergebnis = {}
|
||||
for art in ("input_text", "input_number", "input_boolean", "input_datetime"):
|
||||
eintraege = ha.befehl_ok(f"{art}/list") or []
|
||||
ergebnis[art] = [e for e in eintraege if str(e.get("id", "")).startswith(PRAEFIX)]
|
||||
return ergebnis
|
||||
|
||||
|
||||
def main() -> int:
|
||||
aktion, ha_projekt, trading = sys.argv[1], Path(sys.argv[2]), Path(sys.argv[3])
|
||||
sys.path.insert(0, str(ha_projekt / "src"))
|
||||
import ha_verbindung as hv
|
||||
|
||||
geheimnisse = trading / "config" / "secrets.toml"
|
||||
token = hv.lies_token()
|
||||
print(f"Token gelesen (Laenge {len(token)}), Instanz aus dem HA-Projekt, Aktion {aktion}")
|
||||
|
||||
# nur_lesen=False auch fuers Pruefen: Die Leseliste jenes Moduls kennt die Listenbefehle der
|
||||
# Helfer und get_states nicht. 'pruefen' und 'probe' senden trotzdem nur lesende Befehle.
|
||||
with hv.HaVerbindung(hv.BASIS_URL, token, nur_lesen=False) as ha:
|
||||
vorhanden = helfer_liste(ha)
|
||||
zahl_helfer = sum(len(v) for v in vorhanden.values())
|
||||
automationen = [
|
||||
s for s in (ha.befehl_ok("get_states") or [])
|
||||
if s.get("entity_id", "").startswith("automation.") and s.get("attributes", {}).get("id") == AUTOMATION_ID
|
||||
]
|
||||
abschnitt = lies_abschnitt(geheimnisse)
|
||||
print(f"Helfer mit Praefix {PRAEFIX}: {zahl_helfer} -- " + ", ".join(f"{a}: {[e['id'] for e in v]}" for a, v in vorhanden.items() if v))
|
||||
print(f"Automation {AUTOMATION_ID}: {'vorhanden' if automationen else 'fehlt'}")
|
||||
print("[homeassistant] in der Geheimnisdatei: " + ("fehlt" if abschnitt is None else str({k: (type(v).__name__, len(v) if isinstance(v, str) else v) for k, v in abschnitt.items()})))
|
||||
|
||||
if aktion == "pruefen":
|
||||
return 0
|
||||
|
||||
if aktion == "lesen":
|
||||
# Nur lesen: die Zustaende der Helfer, wie sie gerade in Home Assistant stehen.
|
||||
zustaende = {s["entity_id"]: s["state"] for s in (ha.befehl_ok("get_states") or []) if f".{PRAEFIX}" in s.get("entity_id", "")}
|
||||
for name in sorted(zustaende):
|
||||
print(f" {name} = {zustaende[name]}")
|
||||
return 0
|
||||
|
||||
if aktion == "einrichten":
|
||||
if abschnitt is not None or automationen or zahl_helfer:
|
||||
print("ABBRUCH: Es gibt schon Teile der Einrichtung; erst 'entfernen' oder von Hand klaeren.")
|
||||
return 3
|
||||
for name in TEXTE:
|
||||
ha.befehl_ok("input_text/create", name=name, min=0, max=100, mode="text", icon="mdi:chart-line")
|
||||
for name, kleinst, groesst, schritt, einheit in ZAHLEN:
|
||||
ha.befehl_ok("input_number/create", name=name, min=kleinst, max=groesst, step=schritt, mode="box", unit_of_measurement=einheit, icon="mdi:chart-line")
|
||||
for name in SCHALTER:
|
||||
ha.befehl_ok("input_boolean/create", name=name, icon="mdi:alert-outline")
|
||||
for name in ZEITEN:
|
||||
ha.befehl_ok("input_datetime/create", name=name, has_date=True, has_time=True, icon="mdi:clock-outline")
|
||||
webhook_id = secrets.token_hex(32)
|
||||
status, antwort = ha.http_post(
|
||||
f"/api/config/automation/config/{AUTOMATION_ID}",
|
||||
{
|
||||
"alias": ALIAS,
|
||||
"description": "Uebernimmt Phase, Killswitch, Position und Tagesergebnis des Paper-Runners aus dem Webhook der Trading Plattform. Angelegt am 2026-09-20 ueber ha_einrichten.py; Rueckbau mit der Aktion entfernen.",
|
||||
"trigger": [{"platform": "webhook", "webhook_id": webhook_id, "allowed_methods": ["POST"], "local_only": True}],
|
||||
"condition": [],
|
||||
"action": aktionen(),
|
||||
"mode": "queued",
|
||||
"max": 5,
|
||||
},
|
||||
)
|
||||
print(f"Automation angelegt: HTTP {status}")
|
||||
if status != 200:
|
||||
print("ABBRUCH: Die Automation wurde nicht angenommen:", str(antwort)[:200].replace(webhook_id, "***"))
|
||||
return 2
|
||||
trage_ein(geheimnisse, f"{hv.BASIS_URL}/api/webhook/{webhook_id}")
|
||||
print("Adresse eingetragen ([homeassistant], aktiv = false); die ID wurde nicht ausgegeben.")
|
||||
return 0
|
||||
|
||||
if aktion == "probe":
|
||||
if abschnitt is None:
|
||||
print("ABBRUCH: kein [homeassistant] in der Geheimnisdatei")
|
||||
return 3
|
||||
nutzlast = {
|
||||
"phase": "probe_der_einrichtung", "modus": "probe", "killswitch": "angehalten", "position": 7,
|
||||
"tagesergebnis_usd": -12.34, "herzschlag_alter_s": 5, "veraltet": False,
|
||||
"gesendet": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
}
|
||||
anfrage = urllib.request.Request(
|
||||
abschnitt["webhook_url"], data=json.dumps(nutzlast).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"}, method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(anfrage, timeout=5) as antwort:
|
||||
print(f"Webhook: HTTP {antwort.status}")
|
||||
except Exception as ausnahme:
|
||||
# Nur der Typ: Die Ausnahme traegt die Adresse und damit die ID.
|
||||
print(f"Webhook GESCHEITERT: {type(ausnahme).__name__}")
|
||||
return 2
|
||||
time.sleep(2.0)
|
||||
zustaende = {s["entity_id"]: s["state"] for s in (ha.befehl_ok("get_states") or []) if f".{PRAEFIX}" in s.get("entity_id", "")}
|
||||
for name in sorted(zustaende):
|
||||
print(f" {name} = {zustaende[name]}")
|
||||
erwartet = {
|
||||
"input_text.trading_phase": "probe_der_einrichtung", "input_text.trading_killswitch": "angehalten",
|
||||
"input_number.trading_position": "7.0", "input_number.trading_tagesergebnis_usd": "-12.34",
|
||||
"input_boolean.trading_veraltet": "off",
|
||||
}
|
||||
falsch = {k: (v, zustaende.get(k)) for k, v in erwartet.items() if zustaende.get(k) != v}
|
||||
print("Rueckgelesen wie gesendet." if not falsch else f"BEFUND: {falsch}")
|
||||
return 0 if not falsch else 1
|
||||
|
||||
if aktion == "entfernen":
|
||||
status = ha.http_delete(f"/api/config/automation/config/{AUTOMATION_ID}")
|
||||
print(f"Automation geloescht: HTTP {status}")
|
||||
for art, eintraege in vorhanden.items():
|
||||
for eintrag in eintraege:
|
||||
ha.befehl_ok(f"{art}/delete", **{f"{art}_id": eintrag["id"]})
|
||||
print(f" geloescht: {art}.{eintrag['id']}")
|
||||
if abschnitt is not None:
|
||||
print("Hinweis: [homeassistant] steht weiter in der Geheimnisdatei des Trading-Projekts; von Hand entfernen.")
|
||||
return 0
|
||||
|
||||
print("unbekannte Aktion")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user