Benachrichtigungen: Web Push und E-Mail als Kanaele der Automatiken
Neuer BenachrichtigungTransport hinter der Aktor-URL "Benachrichtigung": push schickt an alle angemeldeten Geraete (homeMesh.push_abos), mail ueber den SMTP-Zugang aus der config.ini. Ein Abo, das der Push-Dienst mit 404 oder 410 ablehnt, wird geloescht statt weiter angeschrieben. Daneben hoert der Runner auf benachrichtigung/# - darueber schickt die Probe aus den Einstellungen, ohne dass die Weboberflaeche verschluesseln muesste. Zugestellt wird im Haupttakt, nicht im MQTT-Faden. pywebpush, py_vapid und http_ece liegen wie die uebrigen Fremdpakete im Ordner, nicht in site-packages; transports.py haengt ihn an den Suchpfad. Das Schluesselpaar erzeugt vapid_erzeugen.py und bleibt ausserhalb des Git. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,8 @@ import pymysql
|
||||
import requests
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from transports import (AUTOMATIK_URL, AutomatikTransport, HTTPTransport,
|
||||
from transports import (AUTOMATIK_URL, BENACHRICHTIGUNG_TOPIC,
|
||||
BenachrichtigungTransport, AutomatikTransport, HTTPTransport,
|
||||
LogicTransport, MQTTTransport, TahomaTransport,
|
||||
WLEDTransport, ausloeser_kennung, ausloeser_url,
|
||||
ist_topic)
|
||||
@@ -113,6 +114,18 @@ class Config:
|
||||
except ValueError:
|
||||
return vorgabe
|
||||
|
||||
def abschnitt(self, sektion):
|
||||
"""
|
||||
Einen ganzen Abschnitt als Dict.
|
||||
|
||||
Fuer den Mailzugang: der besteht aus einem halben Dutzend Feldern und
|
||||
wird als Ganzes an den Transport weitergereicht, statt sechsmal
|
||||
einzeln abgefragt zu werden.
|
||||
"""
|
||||
if not self.cfg.has_section(sektion):
|
||||
return {}
|
||||
return {k: v.strip() for k, v in self.cfg.items(sektion)}
|
||||
|
||||
def ja(self, sektion, schluessel, vorgabe=False):
|
||||
return self.text(sektion, schluessel, str(vorgabe)).lower() in ("true", "1", "yes", "on")
|
||||
|
||||
@@ -755,7 +768,25 @@ class Runner:
|
||||
config.zahl("tahoma", "timeout", 10), self.dry_run),
|
||||
LogicTransport(self.sonnenzeiten),
|
||||
AutomatikTransport(self.ausloesezeiten),
|
||||
BenachrichtigungTransport(self.push_abos, self.push_abo_weg,
|
||||
self.push_abo_erfolg,
|
||||
config.abschnitt("mail"),
|
||||
# Ein leerer Eintrag in der config.ini ist
|
||||
# vorhanden, aber leer - die Vorgabe von
|
||||
# text() greift dann nicht.
|
||||
(config.text("push", "schluessel")
|
||||
or os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "push_vapid.json")),
|
||||
self.dry_run),
|
||||
]
|
||||
# Meldungen, die nicht aus einer Automatik kommen: die Probe aus den
|
||||
# Einstellungen, spaeter vielleicht ein Skript. Derselbe Weg, dieselbe
|
||||
# Zustellung - nur ohne Umweg ueber eine Regel.
|
||||
self.meldungen = queue.Queue()
|
||||
try:
|
||||
self.mqtt.subscribe(BENACHRICHTIGUNG_TOPIC)
|
||||
except Exception as fehler:
|
||||
logger.warning("Meldungs-Topic nicht abonnierbar: %r", fehler)
|
||||
self.regelwerk = None
|
||||
|
||||
# --- Aufbau ----------------------------------------------------------
|
||||
@@ -774,10 +805,83 @@ class Runner:
|
||||
|
||||
def _mqtt_nachricht(self, client, userdata, nachricht):
|
||||
# Der Client laeuft schon, waehrend __init__ noch die Transporte baut.
|
||||
if nachricht.topic.startswith("benachrichtigung/"):
|
||||
# Nicht hier zustellen: Dieser Rueckruf laeuft im MQTT-Faden, und
|
||||
# eine Datenbankverbindung gehoert einem Faden. Der naechste Takt
|
||||
# holt es ab.
|
||||
schlange = getattr(self, "meldungen", None)
|
||||
if schlange is not None:
|
||||
schlange.put((nachricht.topic.rsplit("/", 1)[-1], nachricht.payload))
|
||||
return
|
||||
for transport in getattr(self, "transporte", []):
|
||||
if isinstance(transport, MQTTTransport):
|
||||
transport.nachricht(nachricht.topic, nachricht.payload)
|
||||
|
||||
# --- Benachrichtigungen ----------------------------------------------
|
||||
|
||||
def benachrichtigung(self):
|
||||
"""Der Transport fuer die Meldungen, oder None."""
|
||||
for transport in self.transporte:
|
||||
if isinstance(transport, BenachrichtigungTransport):
|
||||
return transport
|
||||
return None
|
||||
|
||||
def meldungen_abarbeiten(self):
|
||||
"""
|
||||
Was ueber benachrichtigung/# hereinkam, zustellen.
|
||||
|
||||
Nutzlast ist JSON ({"titel": ..., "text": ...}); ein blosser Text
|
||||
geht auch durch und wird zum Textkoerper.
|
||||
"""
|
||||
transport = self.benachrichtigung()
|
||||
while True:
|
||||
try:
|
||||
kanal, rohtext = self.meldungen.get_nowait()
|
||||
except queue.Empty:
|
||||
return
|
||||
if transport is None:
|
||||
continue
|
||||
try:
|
||||
text = rohtext.decode("utf-8", "replace") if isinstance(rohtext, bytes) else str(rohtext)
|
||||
try:
|
||||
daten = json.loads(text)
|
||||
if not isinstance(daten, dict):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
daten = {"text": text}
|
||||
if kanal == "mail":
|
||||
transport.mail_senden(daten.get("betreff") or daten.get("titel") or "Smarthome",
|
||||
daten.get("text") or "")
|
||||
else:
|
||||
transport.push(daten.get("titel") or "Smarthome", daten.get("text") or "")
|
||||
except Exception as fehler:
|
||||
logger.warning("Meldung (%s) nicht zustellbar: %r", kanal, fehler)
|
||||
|
||||
def push_abos(self):
|
||||
"""Die angemeldeten Geraete. Fehlt die Tabelle, gibt es eben keine."""
|
||||
try:
|
||||
with self.db.cursor() as c:
|
||||
c.execute("SELECT id, endpoint, p256dh, auth, name FROM push_abos")
|
||||
return list(c.fetchall())
|
||||
except Exception as fehler:
|
||||
logger.debug("push_abos nicht lesbar: %r", fehler)
|
||||
return []
|
||||
|
||||
def push_abo_weg(self, abo_id, grund):
|
||||
try:
|
||||
with self.db.cursor() as c:
|
||||
c.execute("DELETE FROM push_abos WHERE id = %s", (abo_id,))
|
||||
except Exception as fehler:
|
||||
logger.warning("Push-Abo %s nicht loeschbar: %r", abo_id, fehler)
|
||||
|
||||
def push_abo_erfolg(self, abo_id):
|
||||
try:
|
||||
with self.db.cursor() as c:
|
||||
c.execute("UPDATE push_abos SET zuletzt = NOW(), fehler = '' WHERE id = %s",
|
||||
(abo_id,))
|
||||
except Exception as fehler:
|
||||
logger.debug("Push-Abo %s nicht fortschreibbar: %r", abo_id, fehler)
|
||||
|
||||
def transport_fuer(self, actor_url):
|
||||
for transport in self.transporte:
|
||||
if transport.passt(actor_url):
|
||||
@@ -1311,6 +1415,7 @@ class Runner:
|
||||
while True:
|
||||
jetzt = time.monotonic()
|
||||
self.werte_einsammeln(auch_geraete=nur_einmal)
|
||||
self.meldungen_abarbeiten()
|
||||
self.durchlauf(fenster)
|
||||
self.ergebnisse_verbuchen()
|
||||
self.protokoll_saeubern()
|
||||
|
||||
Reference in New Issue
Block a user