# 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 `