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>
173 lines
5.8 KiB
PHP
173 lines
5.8 KiB
PHP
<?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;
|
|
}
|