# 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. Für einige Teile reicht diese Landkarte nicht, weil ihr Verhalten aus dem Zusammenspiel mehrerer Prozesse entsteht. Sie haben eigene, ausführliche Dokumente unter [`doku/`](doku/README.md): **[Automatiken](doku/automatiken.md)**, **[Zeitleiste](doku/zeitleiste.md)**, **[Benachrichtigungen](doku/benachrichtigungen.md)**, **[Batterie](doku/byd.md)**, **[Meteogramm](doku/meteogramm.md)**, **[Solar-Übersicht](doku/uebersicht.md)**, **[Datenbanken](doku/datenbank.md)** und **[Einstellungsseite](doku/einstellungen.md)**. --- ## 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 │ ├── einfuehrung.php ← die neun Seitenvorlagen │ ├── rooms.php Etagen und Räume lesen (homeMesh), Kachelvorgabe, Symbole │ ├── grundriss.php Etagen, Räume, Kachelpositionen, Bilder ändern (Reiter Grundriss) │ ├── zeitleiste.php Automatiken eines Tages: Lage, Bahn, ob dran, Läufe │ ├── meshdb.php meshDb(): Verbindung zu homeMesh │ ├── costs.php Preistabellen + solarDb() │ ├── automations.php Automatiken, Gerätekatalog │ ├── 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 │ ├── skodaKeys.php MyŠkoda-Schlüssel in skoda.conf (nie zurück an den Browser) │ ├── 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/ mitgelieferte Grundriss-Renderings dieses Hauses, Icons ├── tiles/ beschreibbar, nicht im Git: Kartenkacheln, grundriss/ (hochgeladene Grundrisse) └── *.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`, `anlageKatalog.js`, `anlage.js`, `speicher.js`, `meteogrammKatalog.js`, `meteogramm.js`, `energieflussKatalog.js`, `energiefluss.js`, `bewaesserungAnzeige.js`, `ladefenster.js`, `bedienfenster.js`, `solarMQTT.js` | `speicher`, `meteogramm`, `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` | | `home` | `home.php` | `autoActionFuncs.js`, `zeitleiste.js`, `raumfenster.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 — hier steht noch das eingebettete Meteogramm von meteoblue (`js/meteogram.js`); das eigene gibt es bisher nur auf der Solar-Seite | `weatherStation/#` | | `settings` | `settings.php` | `settings.js`, `grundriss.js`, `energieflussKatalog.js`, `energiefluss.js`, `energieflussEinstellungen.js`, `benachrichtigungen.js` | `settings.php?action=…`, `push.php?action=…` | — | | `logs` | `logs.php` | `logs.js` | `logs.php?action=…` | — | | `einfuehrung` | `einfuehrung.php` | `einfuehrung.js` | nichts — Folien und Bilder stehen in der Seite | — | | `anzeige` | `solar.php` im Anzeige-Modus | wie `solar`, dazu `anzeige.js` | wie `solar` | wie `solar` | **Anzeige-Modus** (`?action=anzeige`) — die Solar-Seite für ein festes Display, gebaut für den Raspberry Pi 3B+ mit 800 × 480 im Kioskmodus. In `index.php` eine Kopie des Eintrags `solar` mit `"anzeige" => true`: `header.php` lässt Kopfleiste und Menü weg und setzt eine schmale Leiste (Übersicht, Anlage, Speicher, Diagramme), `footer.php` den Fuß; die Realtime-Karte füllt einen Bildschirm, die Übersicht bleibt quer. Bedienfenster, Diagramme und Bewässerung kommen unverändert aus `solarMQTT.js`. `anzeige.js` ergänzt nur: zurück zur Übersicht nach zwei Minuten ohne Berührung (offene Fenster schließen), Verbindungspunkt (grün/gelb/rot) und Uhr in der Leiste, Neuladen alle sechs Stunden. Ohne weitere Parameter läuft sie „ruhig“ — nur die Flüsse bewegen sich, in groben Stufen — und ohne Mauszeiger; `&animation=1` schaltet alle Animationen ein, `&maus=1` zeigt den Zeiger. Und dasselbe als Abhängigkeitsbild — von der Seite bis in den Speicher: ```mermaid flowchart TB subgraph S["Seiten"] HOME["home"]; SOLAR["solar"]; HIST["history"]; HEAT["heat"] SKODA["skoda"]; SET["settings"]; LOGS["logs"]; WEA["weather"] end subgraph J["js/solar"] COMMON["common.js
immer geladen"] HOMEJS["homeMQTT.js"]; AUTOJS["autoActionFuncs.js"]; SOLJS["solarMQTT.js"] HISTJS["historyMQTT.js"]; STATJS["jahresstatistik.js"]; HEATJS["heatMQTT.js"] SKODAJS["skodaMQTT.js"]; SETJS["settings.js"]; LOGJS["logs.js"]; WEAJS["weatherMQTT.js"] end subgraph A["ajax"] ROOM["room.php"]; AUTOA["AutoAction.php"]; SETA["settings.php"] STATS["getStats.php"]; EHIST["energyHistory.php"]; PROD["getProdData
getConsData
getForecastData"] HEATA["getHeaterData
getWaterData"]; SKODAA["skoda.php
skodaCmd.php
tile.php"]; LOGA["logs.php"] end subgraph M["restricted (Modell)"] ROOMS["rooms.php"]; COSTS["costs.php"]; AUTOM["automations.php"] KACH["kacheln.php"]; RCTRL["roomControls.php"]; CMD["commands.php"]; LOGD["logdateien.php"] end SOLARLOG[("solarLog")]; HOMEMESH[("homeMesh")]; DATEIEN[/"Logdateien"/]; BROKER{{"Broker"}} HOME --> HOMEJS & AUTOJS SOLAR --> SOLJS & STATJS HIST --> HISTJS & STATJS HEAT --> HEATJS SKODA --> SKODAJS SET --> SETJS LOGS --> LOGJS WEA --> WEAJS S -.-> COMMON HOMEJS --> ROOM AUTOJS --> AUTOA SOLJS --> PROD HISTJS --> EHIST STATJS --> STATS HEATJS --> HEATA SKODAJS --> SKODAA SETJS --> SETA LOGJS --> LOGA ROOM --> RCTRL & CMD & AUTOM AUTOA --> AUTOM SETA --> COSTS & KACH & AUTOM STATS --> COSTS SETA --> GRM["grundriss.php"] SETA --> ENF["energiefluss.php"] ENF --> HOMEMESH KACH --> ROOMS GRM --> ROOMS LOGA --> LOGD COSTS --> SOLARLOG AUTOM --> HOMEMESH CMD --> HOMEMESH CMD -.-> BROKER PROD --> SOLARLOG EHIST --> SOLARLOG HEATA --> SOLARLOG SKODAA --> SOLARLOG LOGD --> DATEIEN classDef speicher fill:#eaf5ee,stroke:#6fa981 class SOLARLOG,HOMEMESH,DATEIEN,BROKER speicher ``` --- ## Die JavaScript-Schicht Kein Framework, kein Bundler. Jede Datei ist ein Skript, das der Browser so lädt, wie es dasteht; geteilt wird über globale Funktionen aus `common.js`. Die Namen sind deutsch, sobald es um die Sache geht — englisch nur dort, wo eine Bibliothek es vorgibt. ### `common.js` — immer geladen | Funktion | wofür | |---|---| | `powerToString(w)` | Leistung als `W` oder `kW`, dieselbe Rundung überall | | `getData(chart, url, opt)` | Diagrammdaten holen, eintragen, alle 5 min auffrischen; optional Sonnenauf-/-untergang aus `getSunrise.php` dazu | | `mountMeteogram()`, `updateWeatherCards()` | Wetterteile der Solar- und Wetterseite | | `reiterMerken()` | offener Reiter steht in der Adresse und übersteht ein Neuladen | | `loadingHTML(text)` | der eine Ladezustand für alle Modals | ### Die Seitenskripte **`homeMQTT.js`** — Grundriss und Kacheln. Zwei Objekte: `homeMQTT` hält die Verbindung und den Nachrichtenbaum (`mqttData`), `homeSVG` schreibt in die SVG-Kacheln. Was auf einer Kachel steht, kommt nicht aus dem Skript, sondern aus `homeRooms` — der Raumtabelle, die `home.php` als JSON in die Seite schreibt. `homeSVG.kachelWert()` ist die Stelle, an der aus einer Nachricht Text wird: Topic oder Wechselrichtersumme lesen, ggf. JSON-Pfad herausgreifen, Vorzeichen drehen (`negativ`), skalieren (`format: "leistung"` für Watt-Quellen, `"leistung_kw"` für Quellen, die schon Kilowatt melden — die Wallboxen), runden (`stellen`), Einheit anhängen. Klick auf eine Kachel öffnet das Raum-Modal über `ajax/room.php?room=`; bedient wird es von `raumfenster.js`. Heizsymbol und Ist-Werte lesen aus dem Thermostat-Zweig des Raums (`mqttZweig()`). **`raumfenster.js`** — das Raum-Modal im Stil von Lade-, Heiz- und Bewässerungsfenster: Reiter-Kacheln mit Zustand, Beschattung mit Auf/Stop/my/Zu, Höhe und Lamellen in 25-%-Stufen, Ziehen im Fensterbild und „aktuellen Stand als my speichern“; Heizung als Ring mit ziehbarem Soll-Punkt; Lampen mit Birne als Schalter, Farbfeldern und Effekten. Jede Wahl gilt sofort. Jalousien werden nach einem Befehl bis zum Stillstand verfolgt (`rfJalousieVerfolgen()`), die Heizwerte kommen über `raumfensterMqtt()` live. **`auswahl.js`** — eine Auswahlliste für die ganze Anwendung. Wird auf jeder Seite geladen (`footer.php`) und ergänzt jedes `` bleibt stehen und hält weiter den Wert, vorhandene `onchange`-Zuhörer merken nichts davon. Die aufgeklappte Liste hängt an `` und liegt mit `position: fixed` über der Seite — im Fluss machte sie ein Modal höher, und der ganze Inhalt sprang. Ab neun Einträgen gibt es ein Suchfeld, Gruppen (``) bleiben Gruppen, die Tastatur bedient sie. Neue `` schreiben — `js/solar/auswahl.js` macht daraus die eigene Liste. Keine zweite Variante bauen | | Neues Symbol auf einer Kachel | Name eines Bootstrap-Icons ins Feld tippen. Vorschlagsliste: `kachelSymbole()` in `kacheln.php` | | Symbol für eine Gerätegruppe (Raum-Modal, Zeitleiste, Editor) | ein Bootstrap-Icon in `raumGruppenTitel()` (`roomControls.php`), `zeitleisteBahnen()` (`zeitleiste.php`) und `GERAETE_SYMBOL` (`autoActionFuncs.js`) — überall dasselbe. Fehlt es in der Icon-Schrift, eine eigene Maske in `css/solar.css` anlegen (Vorbild: `bi-beschattung`) | | Neue Kennzahl in der Jahresstatistik | `KENNZAHLEN` **und** `kennzahlenAus()` in `ajax/getStats.php` — sonst nichts | | Neues Diagramm auf einer Seite | Karte in der Vorlage, ``, dazu ein Endpunkt nach dem Muster von `getProdData.php`, geladen mit `getData()` | | Neue Seite | Zeile in `$pages` (`index.php`), Vorlage unter `restricted/`, Skript unter `js/solar/` | | Anderes Bedienelement für ein Gerät | `bedienform()` in `roomControls.php` (nach Fähigkeit, nicht nach Typname) | | Neue Geräteart erkennen | neues Modul unter `restricted/deviceDiscovery/modules/` + Eintrag in der Liste in `device_discovery.py` | | Neuer Geräteweg zum Schalten | `commands.php` (Browser) **und** `transports.py` im SolarManager-Repo (Runner) | | Aussehen der ganzen Seite | `css/solar.css`, oberster Abschnitt: die `--ton-*`-Tokens und die Bootstrap-Zuordnung | | Reiter irgendwo | `reiterLeiste()`/`reiterBlock()` aus `restricted/reiter.php` benutzen, keine eigene Variante bauen. Ausnahme: das Raum-Modal hat Reiter-Kacheln mit Zustand (`zeichneGruppen()`), passend zu den anderen Bedienfenstern | | Neue Einstellung | Reiter in `$reiter` (`restricted/settings.php`), Abschnitt in `settings.js`, Aktion in `ajax/settings.php`, Modellfunktion daneben | --- ## Entwickeln und Prüfen Der Ordner ist die laufende Anwendung — es gibt keinen Build und keine Staging-Stufe. Entsprechend gilt: ```bash # PHP-Syntax (auf der NAS; /usr/bin/php ist 8.1 ohne mysqli) /usr/local/bin/php82 -l restricted/kacheln.php # JavaScript-Syntax /var/packages/Node.js_v20/target/usr/local/bin/node --check js/solar/settings.js # Modellfunktion einzeln ausprobieren, ohne Browser /usr/local/bin/php82 -r 'require "restricted/automations.php"; print_r(geraeteListe()[0]);' # Datenbank ansehen (readDB, nur SELECT) /usr/local/mariadb10/bin/mysql -h 127.0.0.1 -e 'SELECT * FROM homeMesh.actors LIMIT 5;' ``` Für Masken lohnt eine **Probeseite**: eine kleine HTML-Datei, die `solar.css` und das Skript lädt und `window.fetch` durch eine Attrappe mit echten Daten ersetzt. Damit lässt sich das Zeichnen prüfen, ohne angemeldet zu sein und ohne etwas zu speichern. **Die Einführung** (Menü → Einführung) sind Folien für Einsteiger in zwei Teilen: Bedienen im Alltag, Einrichten und Verwalten. Die Texte stehen in `einfuehrungFolien()` in `restricted/einfuehrung.php`, die Bilder sind echte Aufnahmen unter `assets/img/einfuehrung`. Ändert sich die Oberfläche, nimmt `tools/einfuehrung/aufnehmen.ps1` sie auf dem Arbeitsrechner neu auf — Edge ohne Fenster über das DevTools-Protokoll, mit echten Wartezeiten für die Livewerte, Klicks und Scrollen laut `plan.json`: ```powershell powershell -ExecutionPolicy Bypass -File tools\einfuehrung\aufnehmen.ps1 -Plan tools\einfuehrung\plan.json -Ausgabe assets\img\einfuehrung ``` Zu jedem Bild schreibt es eine `.json` mit der Lage der markierten Elemente; daraus setzt die Seite ihre nummerierten Punkte, sie wandern also mit. Ort und Koordinaten im Wetterkopf sowie die Endungen der Fahrzeugschlüssel deckt der Plan vor der Aufnahme ab. Die Seite muss dafür im Heimnetz erreichbar sein. Zwei Repositories, beide auf Gitea (`gitea.nas.el-wa.org`): | Repo | Ort | Inhalt | |---|---|---| | `admin/Smart-Dashboard` | `/volume1/web/smart` | dieses hier | | `admin/SolarManager` | `/volume1/homes/wagner/SolarManager` | Sammler, Brücken, Automatik-Runner | Rekursive Suchen (`grep -r`) laufen über die SMB-Freigabe in Zeitüberschreitungen — auf der NAS selbst sind sie sofort fertig.