Benachrichtigungen: Geraet, Abos und der Reiter Meldungen
Der Geraetesuchlauf legt "Benachrichtigungen" an (benachrichtigung_module.py), ein Kommando je Kanal. Damit kann jede Automatik melden, ohne dass Editor, Zeitleiste oder Datenbank etwas davon wissen muessten. Fuer die Meldung aufs Handy braucht es keine App: Der Browser meldet sich unter Einstellungen -> Meldungen selbst an (sw.js, ajax/push.php), das Abo steht in homeMesh.push_abos, verschickt wird vom Runner. Auf dem iPhone geht es erst ab der Ablage auf dem Home-Bildschirm - der Reiter sagt das auch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* Modell-Schicht für die Web-Push-Abos.
|
||||
*
|
||||
* Ein Abo ist ein Gerät, das Meldungen bekommen möchte. Erzeugt wird es im
|
||||
* Browser (js/solar/benachrichtigungen.js), gespeichert hier, benutzt vom
|
||||
* Runner (BenachrichtigungTransport in SolarManager/autoActions/transports.py).
|
||||
*
|
||||
* Die Schlüssel liegen in push_vapid.json neben skoda.conf - außerhalb des
|
||||
* Web-Roots, weil der private Teil dort steht. Von hier wird nur der
|
||||
* öffentliche gelesen; er gehört ohnehin in jeden Browser.
|
||||
*
|
||||
* Tabelle: homeMesh.push_abos, siehe homeMesh_push.sql.
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . "/meshdb.php");
|
||||
|
||||
/** Wo das Schlüsselpaar liegt. Erzeugt von SolarManager/vapid_erzeugen.py. */
|
||||
defined("PUSH_SCHLUESSEL") || define("PUSH_SCHLUESSEL",
|
||||
"/volume1/homes/wagner/SolarManager/push_vapid.json");
|
||||
|
||||
/**
|
||||
* Der öffentliche VAPID-Schlüssel, oder "" wenn es noch keinen gibt.
|
||||
*
|
||||
* Ohne ihn kann der Browser kein Abo anlegen; die Maske sagt dann, was zu
|
||||
* tun ist, statt einen leeren Knopf anzubieten.
|
||||
*/
|
||||
function pushSchluessel()
|
||||
{
|
||||
static $wert = null;
|
||||
if ($wert !== null) {
|
||||
return $wert;
|
||||
}
|
||||
$wert = "";
|
||||
if (is_readable(PUSH_SCHLUESSEL)) {
|
||||
$daten = json_decode(file_get_contents(PUSH_SCHLUESSEL), true);
|
||||
if (is_array($daten) && !empty($daten["public_key"])) {
|
||||
$wert = strval($daten["public_key"]);
|
||||
}
|
||||
}
|
||||
return $wert;
|
||||
}
|
||||
|
||||
/** Gibt es die Tabelle? Eine Installation ohne homeMesh_push.sql soll nicht scheitern. */
|
||||
function pushTabelleDa()
|
||||
{
|
||||
static $da = null;
|
||||
if ($da === null) {
|
||||
$res = meshDb()->query("SHOW TABLES LIKE 'push_abos'");
|
||||
$da = $res && $res->num_rows > 0;
|
||||
}
|
||||
return $da;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Abos - ohne die Geheimnisse.
|
||||
*
|
||||
* Endpoint und Schlüssel bleiben auf dem Server: Wer den Endpoint hat, kann
|
||||
* dem Gerät schreiben. Die Maske braucht nur Name, Browser und Stand.
|
||||
*/
|
||||
function pushAbos()
|
||||
{
|
||||
if (!pushTabelleDa()) {
|
||||
return [];
|
||||
}
|
||||
$liste = [];
|
||||
$res = meshDb()->query("SELECT id, name, browser, angelegt, zuletzt, fehler
|
||||
FROM push_abos ORDER BY angelegt");
|
||||
while ($z = $res->fetch_assoc()) {
|
||||
$liste[] = [
|
||||
"id" => intval($z["id"]),
|
||||
"name" => $z["name"],
|
||||
"browser" => $z["browser"],
|
||||
"angelegt" => $z["angelegt"],
|
||||
"zuletzt" => $z["zuletzt"],
|
||||
"fehler" => $z["fehler"],
|
||||
];
|
||||
}
|
||||
return $liste;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Abo anlegen oder auffrischen.
|
||||
*
|
||||
* Der Endpoint ist der Schlüssel: Derselbe Browser meldet sich nach einem
|
||||
* Neustart mit demselben Endpoint, manchmal aber mit neuen Schlüsseln -
|
||||
* dann werden sie überschrieben, statt eine zweite Zeile anzulegen. Ein
|
||||
* erneutes Anmelden setzt außerdem einen alten Fehler zurück.
|
||||
*/
|
||||
function pushAboSpeichern($abo, $name, $browser)
|
||||
{
|
||||
if (!pushTabelleDa()) {
|
||||
throw new RuntimeException("Die Tabelle push_abos fehlt - homeMesh_push.sql einspielen.");
|
||||
}
|
||||
$endpoint = trim(strval($abo["endpoint"] ?? ""));
|
||||
$p256dh = trim(strval($abo["keys"]["p256dh"] ?? ""));
|
||||
$auth = trim(strval($abo["keys"]["auth"] ?? ""));
|
||||
if ($endpoint === "" || $p256dh === "" || $auth === "") {
|
||||
throw new InvalidArgumentException("Das Abo ist unvollständig.");
|
||||
}
|
||||
if (!preg_match('#^https://#', $endpoint) || strlen($endpoint) > 500) {
|
||||
throw new InvalidArgumentException("Diese Adresse taugt nicht als Push-Ziel.");
|
||||
}
|
||||
$name = mb_substr(trim(strval($name)), 0, 80);
|
||||
$browser = mb_substr(trim(strval($browser)), 0, 120);
|
||||
|
||||
$db = meshDb();
|
||||
$stmt = $db->prepare("INSERT INTO push_abos (endpoint, p256dh, auth, name, browser)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE p256dh = VALUES(p256dh), auth = VALUES(auth),
|
||||
name = VALUES(name), browser = VALUES(browser), fehler = ''");
|
||||
$stmt->bind_param("sssss", $endpoint, $p256dh, $auth, $name, $browser);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Ein Abo nach Endpoint entfernen - das tut der Browser beim Abmelden. */
|
||||
function pushAboEntfernenNachEndpoint($endpoint)
|
||||
{
|
||||
if (!pushTabelleDa()) {
|
||||
return 0;
|
||||
}
|
||||
$db = meshDb();
|
||||
$stmt = $db->prepare("DELETE FROM push_abos WHERE endpoint = ?");
|
||||
$stmt->bind_param("s", $endpoint);
|
||||
$stmt->execute();
|
||||
$weg = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
return $weg;
|
||||
}
|
||||
|
||||
/** Ein Abo nach Nummer entfernen - das tut die Maske für ein fremdes Gerät. */
|
||||
function pushAboEntfernen($id)
|
||||
{
|
||||
if (!pushTabelleDa()) {
|
||||
return 0;
|
||||
}
|
||||
$db = meshDb();
|
||||
$stmt = $db->prepare("DELETE FROM push_abos WHERE id = ?");
|
||||
$id = intval($id);
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$weg = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
return $weg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Probemeldung an alle Abos schicken.
|
||||
*
|
||||
* Verschickt wird nicht hier, sondern vom Runner: Er hat die Pakete
|
||||
* (pywebpush) und den privaten Schlüssel, und er ist ohnehin der, der im
|
||||
* Betrieb meldet. Die Weboberfläche legt den Auftrag nur ins MQTT - dasselbe
|
||||
* Topic, das auch eine Automatik benutzen würde.
|
||||
*/
|
||||
function pushProbe($titel, $text)
|
||||
{
|
||||
// MQTT_BROKER und MQTT_PORT stehen in commands.php - dort, wo auch die
|
||||
// Kommandos der Geraete ihren Weg zum Broker finden.
|
||||
require_once(__DIR__ . "/commands.php");
|
||||
require_once(__DIR__ . "/../ajax/phpMQTT.php");
|
||||
$mqtt = new Bluerhinos\phpMQTT(MQTT_BROKER, MQTT_PORT, "PushProbe_" . getmypid());
|
||||
if (!$mqtt->connect(true, null, "", "")) {
|
||||
throw new RuntimeException("Keine Verbindung zum MQTT-Broker.");
|
||||
}
|
||||
$mqtt->publish("benachrichtigung/push",
|
||||
json_encode(["titel" => $titel, "text" => $text], JSON_UNESCAPED_UNICODE),
|
||||
0, false);
|
||||
$mqtt->close();
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user