diff --git a/README.md b/README.md new file mode 100644 index 0000000..512a151 --- /dev/null +++ b/README.md @@ -0,0 +1,641 @@ +# Smart-Dashboard + +Die Anzeige und Bedienung des Hauses: Grundrisse mit Messwerten, Solar- und +Verbrauchsverläufe, das Fahrzeug, die Automatiken, die Einstellungen. Alles, +was einen offenen Browser braucht, liegt hier. + +Gegenstück ist das Repo **`admin/SolarManager`** unter +`/volume1/homes/wagner/SolarManager` — dort laufen die Hintergrundprozesse, +die Messwerte einsammeln und die Automatiken ausführen. Beide Seiten reden +über zwei Dinge miteinander und nie direkt: den **MQTT-Broker** für alles +Aktuelle, die **Datenbanken** für alles, was bleiben soll. Wer wissen will, +woher eine Zahl ursprünglich kommt, liest die README dort; hier steht, was +mit ihr im Browser passiert. + +Dieses Verzeichnis ist zugleich der Web-Root der NAS +(`/volume1/web/smart`, ausgeliefert unter `https://nas.el-wa.org/smart`). +Der Arbeitsordner *ist* die laufende Anwendung — jede gespeicherte Datei ist +sofort live. + +--- + +## Inhalt + +1. [Auf einen Blick](#auf-einen-blick) +2. [Wie eine Seite entsteht](#wie-eine-seite-entsteht) +3. [Zwei Wege für Daten](#zwei-wege-für-daten) +4. [Die Seitenkarte](#die-seitenkarte) +5. [Die JavaScript-Schicht](#die-javascript-schicht) +6. [Die PHP-Schicht](#die-php-schicht) +7. [Die Datenbanken](#die-datenbanken) +8. [Vier Bausteine im Detail](#vier-bausteine-im-detail) +9. [Wiederkehrende Bauteile und Konventionen](#wiederkehrende-bauteile-und-konventionen) +10. [Wo fange ich an, wenn ich … ändern will](#wo-fange-ich-an-wenn-ich--ändern-will) +11. [Entwickeln und Prüfen](#entwickeln-und-prüfen) + +--- + +## Auf einen Blick + +``` +smart/ +├── index.php Router: Anmeldung prüfen, Seite auswählen, drei Includes +├── helper.php Session, Heimnetzerkennung, checkLogin(), httpGetJson() +├── authServer.php Passkeys (WebAuthn): Registrierung und Anmeldung +├── addUser.php Einmal-Link zum Anlegen eines neuen Schlüssels +├── .htaccess sperrt .git, *.py, *.sql, *.ini … für den Browser +│ +├── restricted/ Vorlagen (was man sieht) und Modelle (was es weiß) +│ ├── header.php footer.php Rahmen jeder Seite, Menü, Skript-Einbindung +│ ├── home.php solar.php heat.php history.php +│ ├── skoda.php weather.php settings.php logs.php ← die acht Seitenvorlagen +│ ├── rooms.php Räume, Etagen, Kachelwerte, Symbole +│ ├── costs.php Preistabellen + solarDb() +│ ├── automations.php Automatiken, Gerätekatalog + meshDb() +│ ├── kacheln.php Anzeigewerte der Home-Kacheln (Modell zur Maske) +│ ├── roomControls.php Bedienelemente des Raum-Modals +│ ├── commands.php ein Kommando tatsächlich abschicken +│ ├── wallboxen.php beide Wallboxen über MQTT +│ ├── logdateien.php Logdateien lesen +│ ├── aussenplan.php Schrägbild des Außengeländes (SVG, gerechnet) +│ ├── reiter.php das eine Reiter-Bauteil der ganzen Seite +│ ├── config.php Zahlen, die an der Wirklichkeit hängen +│ ├── mysql.php Zugangsdaten — NICHT im Git (mysql.php.example) +│ └── deviceDiscovery/ Python: Geräte suchen und in homeMesh eintragen +│ +├── ajax/ Endpunkte. Verteiler, keine Logik (Ausnahmen unten) +├── js/solar/ der eigene JavaScript-Code +├── js/ Fremdbibliotheken, alle lokal (kein CDN) +├── css/solar.css das eigene Stylesheet (Theme + alle Bauteile) +├── assets/img/ Grundriss-Renderings OG/EG/UG/AG, Icons +├── tiles/ Kartenkacheln-Zwischenspeicher (nicht im Git) +└── *.sql Schema-Dateien zum Nachziehen von Hand +``` + +--- + +## Wie eine Seite entsteht + +```mermaid +sequenceDiagram + autonumber + participant B as Browser + participant I as index.php + participant H as helper.php + participant T as Vorlage + Modelle + participant F as footer.php + + B->>I: GET index.php?action=home&floor=OG + I->>H: checkLogin() + H-->>I: Heimnetz? sonst Passkey-Sitzung + Note over I: $pages[...] wählt Vorlage,
Bibliotheken und Seitenskripte + I->>T: include header.php + T-->>B: Kopf und Menü, gebaut aus $floors + I->>T: include restricted/(seite).php + T-->>B: Karten, SVG, eingebettete JSON-Daten + I->>F: include footer.php + F-->>B: Bootstrap, ggf. mqtt.js/Chart.js/Leaflet,
common.js, dann die Seitenskripte + B->>B: Skripte verbinden sich mit Broker und holen per fetch nach +``` + +**Anmeldung.** `helper.php` kennt zwei Wege. Aus dem Heimnetz +(`LOCAL_NETWORKS`, geprüft binär über `inet_pton`, ausschließlich anhand von +`REMOTE_ADDR` — Weiterleitungs-Header sind fälschbar und werden bewusst +ignoriert) ist man ohne Anmeldung drin. Von außen zählt ein Passkey: +`authServer.php` mit der WebAuthn-Bibliothek unter `restricted/WebAuthn`, +Schlüssel in der Datenbank `Logins`. Ein neuer Schlüssel entsteht nur über +einen Einmal-Link (`addUser.php`, Tabelle `addUser`, eine Minute gültig). + +**Die Seitentabelle** in `index.php` ist die einzige Stelle, an der steht, +welche Seite es gibt und was sie braucht: + +```php +"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, + "meteogram" => false, "leaflet" => false, + "scripts" => ["js/solar/autoActionFuncs.js", "js/solar/homeMQTT.js"]], +``` + +`footer.php` lädt daraus die Bibliotheken — Chart.js nur wo gezeichnet wird, +mqtt.js nur wo Livewerte laufen, Leaflet nur auf der Fahrzeugseite. Danach +kommt immer `common.js` und dann die Seitenskripte, jeweils mit +`?v=`, damit ein Browser nie eine alte Fassung behält. + +**Vorlage und Modell sind getrennt.** Eine Seitenvorlage baut Markup und ruft +dafür Funktionen aus den Modelldateien; die Modelldateien enthalten kein +Markup außer dort, wo sie ein Bedienelement zeichnen (`roomControls.php`, +`reiter.php`). Die Endpunkte in `ajax/` benutzen dieselben Modellfunktionen — +so kann eine Maske nicht etwas anderes behaupten als der Endpunkt, der sie +speichert. + +--- + +## Zwei Wege für Daten + +```mermaid +flowchart LR + subgraph Browser + JS["js/solar/*.js"] + SVG["SVG-Grundriss
Diagramme
Masken"] + end + + BROKER{{"MQTT-Broker
wss://mqtt.nas.el-wa.org:443"}} + AJAX["ajax/*.php"] + MODELL["restricted/*.php
Modell-Schicht"] + SOLARLOG[("solarLog
Verlauf")] + HOMEMESH[("homeMesh
Geräte, Automatiken")] + GERAETE["Geräte
Tahoma · WLED · Shelly · Wallboxen"] + + BROKER -->|"laufende Werte"| JS + JS --> SVG + JS <-->|"fetch: Verlauf, Struktur, Masken"| AJAX + AJAX --> MODELL + MODELL --> SOLARLOG + MODELL --> HOMEMESH + MODELL -.->|"Kommandos"| BROKER + MODELL -.->|"HTTP · Tahoma · WLED"| GERAETE + + classDef speicher fill:#eaf5ee,stroke:#6fa981 + classDef code fill:#fff6e5,stroke:#d0a548 + class SOLARLOG,HOMEMESH,BROKER speicher + class JS,AJAX,MODELL code +``` + +Die Regel dahinter, und sie gilt ohne Ausnahme: + +| Was | Weg | Warum | +|---|---|---| +| Aktueller Messwert (Leistung, Temperatur, Ladestand) | **MQTT über `wss`, direkt vom Broker** | Er weiß es ohnehin, und er sagt es von selbst — kein Nachfragen im Takt | +| Verlauf, Statistik, Gerätelisten, Formulare | **`fetch` auf `ajax/*.php`** | Das weiß nur die Datenbank | +| Jeder Knopfdruck (schalten, speichern, Fahrzeugbefehl) | **immer über den Server** | Zugangsdaten dürfen die Seite nicht verlassen, und der Broker ist von außen nicht erreichbar | + +Der Browser abonniert je nach Seite `solarManager/#`, `weatherStation/#`, +`wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` und — auf der Home-Seite — +genau die Topics, die auf den Kacheln stehen (`tileTopics()` in `rooms.php`). + +--- + +## Die Seitenkarte + +| Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT | +|---|---|---|---|---| +| `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `solarMQTT.js` | `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` | +| `home` | `home.php` | `autoActionFuncs.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` | +| `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics | +| `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — | +| `skoda` | `skoda.php` | `skodaMQTT.js` | `skoda.php?was=…`, `skodaCmd.php`, `tile.php` | `solarManager/#` | +| `weather` | `weather.php` | `weatherMQTT.js` | nichts — das Meteogramm ist ein fremdes Dokument im `