Files
Smart-Dashboard/README.md
T
adminandClaude Opus 5 b01e938905 Automatik-Editor als drei Baender, mit laufenden Werten
Das Akkordeon schloss beim Oeffnen die anderen Faecher - Ausloeser und
Aktion waren also nie gleichzeitig zu sehen, ausgerechnet bei einer
Regel, die genau daraus besteht. Und das mittlere Fach hiess
"Bedingungen", enthielt aber die Rahmenbedingungen; die echten standen
im ersten. Jetzt drei sichtbare Baender: Wenn, Wann, Dann, gebaut aus
.card mit farbigem Rand und .card-header.

Die Bedingungszeile magert von fuenf gleich schweren Kaesten auf zwei
ab. Das Geraet steht klein und matt ueber dem Messwert - es beantwortet
"woher", nicht "was" -, der Messwert ist Text mit gepunkteter Linie,
und nur Vergleich und Wert sehen noch wie Eingabefelder aus. Dort
steckt die Aussage der Bedingung, dort tippt man auch. Die Einheit
sitzt im Wertfeld statt in einem sechsten Kasten, der Papierkorb
erscheint erst beim Ueberfahren.

Neben jeder Bedingung laeuft ihr aktueller Wert mit. ?action=werte
liefert alle acht Sekunden {state_id: {value, unit}} aus actor_states -
bewusst von dort und nicht ueber MQTT, weil dort nur ein Teil der
Geraete auftaucht: die Jalousien haengen an der Tahoma-Box, die
gerechneten Werte an gar nichts. Das ersetzt den Satz, der frueher am
Ende stand und die Regel Wort fuer Wort nacherzaehlte: statt zu
wiederholen, was darueber steht, beantwortet die Zeile die Frage, die
man wirklich hat - warum laeuft die Automatik gerade nicht?

Bei time, date, datetime, deltatime und elapsed faellt der Browser
bewusst kein Urteil. Ob "Uhrzeit um 07:30" zutrifft, haengt am
Nachholfenster, am Tagesrand und bei elapsed am echten Abstand seit der
letzten Ausloesung - das steht im Runner. Es hier nachzubauen hiesse,
eine zweite Wahrheit zu pflegen, die auseinanderlaeuft. Diese Zeilen
bekommen einen gestrichelten Punkt statt einer Aussage.

Der Takt malt nur nach, statt neu zu bauen: renderConditions() ersetzt
den ganzen Block, damit waeren alle acht Sekunden eine offene
Geraeteliste weg und der Cursor aus einem Wertfeld, in das man gerade
tippt.

Der Rahmen steht zusammengefaltet da, solange nichts von der Vorgabe
abweicht - und das ist der Normalfall; vorher nahm er die meiste
Flaeche fuer den seltensten Inhalt. Aufgeklappt tauschen die drei
Vorlagen (taeglich, werktags, Wochenende) mit der Kurzfassung den
Platz. Sie stehen ausserhalb des Elements mit data-bs-toggle, und das
ist kein Zufall: Bootstrap haelt seinen Umschalter am Dokument in der
Capture-Phase, ein stopPropagation() im Knopf kaeme grundsaetzlich zu
spaet.

Getragen wird das von vier Variablen in solar.css, damit der Rest der
Seite mitzieht statt hinterherzuhinken: --ton-radius auf .875rem,
--ton-radius-klein neu auf .5rem (beide an Bootstraps
--bs-border-radius und -sm), --font-anzeige auf Poppins fuer alle
Ueberschriften. Poppins lag seit jeher unter assets/fonts und trug
bisher die vier Etagenknoepfe im Aussenplan - eine ganze Schrift fuer
vier Buchstaben. Fliesstext und Zahlenkolonnen bleiben beim Theme, weil
Poppins keine Tabellenziffern mitbringt.

Drei neue Klassen stehen absichtlich in solar.css und nicht im Editor.
.feld-als-text ist ein Auswahlfeld, das wie Text aussieht, bis man es
anfaehrt - dieselbe Not haben das Raum-Modal und die
Kachel-Einstellungen. .punkt ist an oder aus. .wert-jetzt ist die eine
echte Ausnahme und als einzelne Klasse auch als solche erkennbar. Der
Satz im Wann-Band braucht gar keine: .callout gibt es in AdminLTE, und
seine Toene kommen aus Bootstraps *-bg-subtle-Variablen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 20:08:50 +02:00

787 lines
38 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,<br/>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,<br/>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=<filemtime>`, 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<br/>Diagramme<br/>Masken"]
end
BROKER{{"MQTT-Broker<br/><small>wss://mqtt.nas.el-wa.org:443</small>"}}
AJAX["ajax/*.php"]
MODELL["restricted/*.php<br/><small>Modell-Schicht</small>"]
SOLARLOG[("solarLog<br/><small>Verlauf</small>")]
HOMEMESH[("homeMesh<br/><small>Geräte, Automatiken</small>")]
GERAETE["Geräte<br/><small>Tahoma · WLED · Shelly · Wallboxen</small>"]
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 `<iframe>` (`js/meteogram.js`) | `weatherStation/#` |
| `settings` | `settings.php` | `settings.js` | `settings.php?action=…` | — |
| `logs` | `logs.php` | `logs.js` | `logs.php?action=…` | — |
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<br/><small>immer geladen</small>"]
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<br/>getConsData<br/>getForecastData"]
HEATA["getHeaterData<br/>getWaterData"]; SKODAA["skoda.php<br/>skodaCmd.php<br/>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
KACH --> ROOMS
ROOMS --> COSTS
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 |
| `submitFormAjax(form)` | Formular abschicken, ohne die Seite zu verlassen |
| `bindModalSlider()` | Schieberegler im Modal an ihre Anzeige koppeln |
| `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"`), runden (`stellen`), Einheit anhängen. Klick auf eine Kachel
öffnet das Raum-Modal über `ajax/room.php?room=EG_Bad`.
**`solarMQTT.js`** — die große Anlagenübersicht. `solarSVG` zeichnet den
Energiefluss (Breite der Pfeile aus der Leistung), darunter hängen die drei
Diagramme Prognose, Verbrauch und Erzeugung über `getData()`. Änderungen am SVG laufen über kleine Setter
(`setText`, `setAttr`, `setFlowWidth`) und werden pro Bildrahmen gesammelt
(`solarSvgUpdatePending`), damit ein Schwall MQTT-Nachrichten nicht hundertmal
Layout auslöst.
**`skodaMQTT.js`** — Fahrzeugseite. Holt alles über `ajax/skoda.php?was=…`
(live, ladungen, kurve, gesundheit, fahrten, strecke), zeichnet das Fahrzeug
als SVG-Draufsicht mit Ladestand im Akku, Ladeknopf und Klimaknopf, und die
Fahrtenkarte mit Leaflet über den eigenen Kachelspeicher `ajax/tile.php`.
Befehle gehen per POST an `ajax/skodaCmd.php` — nie direkt an die API.
**`jahresstatistik.js`** — ein Bauteil, zwei Seiten. `baueJahresstatistik()`
baut aus der Antwort von `getStats.php` die Vergleichstabelle (Jahre
nebeneinander, Gesamtspalte, Δ in Prozentpunkten bei Quoten, Sparklines je
Monat). Die Kennzahlen stehen **nicht** im Skript: sie beschreiben sich in der
Antwort selbst.
**`autoActionFuncs.js`** — der Automatik-Editor: Gerätewähler mit Suche,
Bedingungen in UND-/ODER-Gruppen, Aktionen mit Parametern, Klartext-Vorschau.
Er bekommt Gerätekatalog und Datensatz in einem Dokument von
`AutoAction.php?action=editor` — früher kamen je Bedingung zwei weitere
Anfragen dazu, und die Auswahlfelder füllten sich asynchron. Dazu die
Übersicht: Nachfolger einklappen (`toggleAutomationGroup`) und die Rückfrage
vor dem Löschen (`frageNachfolger`).
**`settings.js`** — drei Reiter in einer Datei, deutlich getrennt: Preistabellen,
Home-Kacheln, Geräte. Jeder Abschnitt hat sein Modell (`kachelModell`,
`geraeteModell`), seine `zeichne…()`-Funktion und eine `…Binden()`-Funktion,
die genau einen Ereignisbehandler auf den Container legt (Delegation) —
deshalb überlebt Bedienung jedes Neuzeichnen.
**`heatMQTT.js`, `historyMQTT.js`, `weatherMQTT.js`, `logs.js`** — jeweils
klein: Diagramme füllen bzw. Logzeilen nachladen und einfärben.
---
## Die PHP-Schicht
### Endpunkte (`ajax/`)
| Endpunkt | Aufruf | liefert / tut |
|---|---|---|
| `room.php` | `?room=EG_Bad`, `POST ?action=command\|temp` | Raum-Modal bauen; ein Kommando oder eine Solltemperatur senden |
| `AutoAction.php` | `?action=editor\|list\|followers\|werte`, `POST save\|delete\|toggle` | Automatik-Editor und -Übersicht |
| `settings.php` | `?action=list\|kacheln\|geraete`, `POST save\|kacheln-save\|geraete-raeume\|geraet-loeschen` | Einstellungsseite |
| `getStats.php` | `GET` | Jahresstatistik, alle Jahre, mit Metadaten je Kennzahl |
| `energyHistory.php` | `?series=prod\|cons&range=month\|year\|decade` | Verbrauchs-/Erzeugungsverlauf (Rohdaten oder Stundenarchiv) |
| `getProdData` `getConsData` `getForecastData` `getHeaterData` `getWaterData` | `?FROM=&TO=` | Chart.js-Datensätze für die jeweilige Karte |
| `getSunrise.php` | `?FROM=&TO=` | Sonnenauf- und -untergänge als Diagramm-Markierungen |
| `skoda.php` | `?was=live\|ladungen\|kurve\|gesundheit\|fahrten\|strecke` | Fahrzeugauswertungen aus `solarLog.skoda` |
| `skodaCmd.php` | `POST befehl=…` | Laden/Klima/Lüftung am Fahrzeug |
| `tile.php` | `?z=&x=&y=` | Kartenkachel aus dem eigenen Zwischenspeicher, sonst einmalig von OSM |
| `logs.php` | `?action=list\|read` | Zustand und Zeilen der Logdateien |
| `carEG.php` `carOG.php` `carSteuerung.php` `heater.php` `watering.php` `tahoma.php` | | Wallboxen, Heizstab, Bewässerung, Tahoma-Bedienung |
| `chartData.php` | (kein Endpunkt) | gemeinsame Bausteine der Chart-Endpunkte |
| `phpMQTT.php` | (Bibliothek) | MQTT aus PHP heraus, für Kommandos |
Die meisten Endpunkte sind **Verteiler**: Eingabe prüfen, Modellfunktion
rufen, JSON zurückgeben. Wo Logik in einem Endpunkt steht, hat das einen
Grund, der oben in der Datei erklärt ist (`getStats.php` rechnet die
Kennzahlen, `energyHistory.php` wählt die Quelle nach Zeitraum,
`skoda.php` bildet Fahrten und Ladungen aus einer unregelmäßigen Zeitreihe).
### Modelle (`restricted/`)
| Datei | Zuständig für | Wichtige Funktionen |
|---|---|---|
| `rooms.php` | Räume, Etagen, Kachelwerte, Symbole | `allRooms()`, `roomsWithTile()`, `tileTopics()`, `kachelVorgabe()`, `kachelUeberschreibungen()`, `bootstrapIcons()`, `kachelIcon()` |
| `costs.php` | Preistabellen, Verbindung zu `solarLog` | `solarDb()`, `preiseLaden()`, `preiseSpeichern()`, `grundpreisImZeitraum()` |
| `automations.php` | Automatiken, Gerätekatalog, Verbindung zu `homeMesh` | `meshDb()`, `deviceCatalog()`, `loadAutomation()`, `saveAutomation()`, `deleteAutomation()`, `automationFollowers()`, `pruefeKreis()`, `kalendertagChoices()`, `tagVorlagen()`, `stateValues()`, `geraeteListe()`, `geraetLoeschen()`, `saveRooms()` |
| `kacheln.php` | Anzeigewerte der Home-Kacheln (Maskenseite) | `kachelListe()`, `kachelKatalog()`, `kachelWertPruefen()`, `kachelWerteSpeichern()`, `kachelSymbole()` |
| `roomControls.php` | Welches Gerät welches Bedienelement bekommt | `bedienform()`, `zeichneBeschattung()`, `zeichneLicht()`, `zeichneMesswerte()` |
| `commands.php` | Ein Kommando abschicken (MQTT/WLED/HTTP/Tahoma) | `executeCommand()`, `sendeMqtt()`, `sendeWled()`, `sendeHttp()`, `sendeTahoma()` |
| `wallboxen.php` | Beide Wallboxen über MQTT | `wallboxen()`, `wallboxSetzen()` |
| `logdateien.php` | Logdateien vom Ende her lesen | `logDateien()`, `logLesen()`, `logZerlegen()` |
| `aussenplan.php` | Schrägbild des Außengeländes | Projektion + SVG |
| `reiter.php` | Reiter | `reiterLeiste()`, `reiterFlaecheAuf/Zu()`, `reiterBlock()` |
| `config.php` | Zahlen, die an der Wirklichkeit hängen (welches Auto an welcher Box) | |
| `mysql.php` | Zugangsdaten, **nicht im Git** | Vorlage: `mysql.php.example` |
Include-Graph der Modellschicht — flach gehalten, damit man einer Zeile bis zur
Datenbank folgen kann, ohne zu springen:
```mermaid
flowchart LR
HELPER["helper.php"] --> MYSQL["mysql.php<br/><small>Zugangsdaten</small>"]
ROOMS["rooms.php"] --> COSTS["costs.php"] --> MYSQL
KACH["kacheln.php"] --> ROOMS
KACH --> COSTS
KACH -. "nur wenn vorhanden" .-> AUTOM["automations.php"]
AUTOM --> MYSQL
AUTOM --> ROOMS
CMD["commands.php"] --> MYSQL
CMD --> TAH["tahoma_EG.php"]
CMD --> PHPMQTT["ajax/phpMQTT.php"]
RCTRL["roomControls.php"] --> REITER["reiter.php"]
HOME["home.php"] --> ROOMS
HOME --> REITER
SET["settings.php"] --> COSTS & KACH & REITER
HEADER["header.php"] --> ROOMS
```
---
## Die Datenbanken
Drei Schemata, klar getrennte Zuständigkeit:
```mermaid
erDiagram
actors ||--o{ actor_states : "meldet"
actors ||--o{ actor_commands : "kann"
actor_commands ||--o{ command_parameters : "nimmt"
automations ||--o{ automation_conditions : "wenn"
automations ||--o{ automation_actions : "dann"
automation_conditions }o--|| actor_states : "vergleicht"
automation_actions }o--|| actor_commands : "schickt"
automation_actions ||--o{ automation_action_params : "mit"
```
**`homeMesh`** — Geräte und Automatiken (Schema: `homeMesh_DB-layout.sql`,
`homeMesh_automations.sql`).
| Tabelle | Inhalt | geschrieben von |
|---|---|---|
| `actors` | ein Gerät je Zeile, `url` bestimmt den Weg (`mqtt://`, `http://`, `wled://`, `io://`, `Logic`) | Discovery; `room` von Hand (Einstellungen → Geräte) |
| `actor_states` | Messwerte: `url` = Topic **oder** Feldname, `value_path` = Schlüssel darin, `current_value` | Discovery (Adressen), Runner (Werte) |
| `actor_commands`, `command_parameters` | was ein Gerät kann und welche Parameter dazugehören | Discovery |
| `automations`, `automation_conditions`, `automation_actions`, `automation_action_params` | das Regelwerk | Web-Editor |
| `automation_log` | was wann ausgelöst hat | Runner |
| `calendar_days` | Feiertage und Ferien | `fetch_calendar.py` (SolarManager) |
| `state_types` | Datentypen der Messwerte (Zahl, Text, Auswahl …) | fest |
**`solarLog`** — alles, was einen Verlauf hat.
| Tabelle | Inhalt | geschrieben von |
|---|---|---|
| `EnergyFlow`, `EnergyFlow_hourly` | Leistungen im Rohtakt und als Stundenarchiv | `solarManager.py` |
| `stats_daily` | Tageswerte für die Jahresstatistik | `solarManager.py` |
| `Heater`, `puffertemp`, `wasser`, `zisterne`, `windrad`, `kiga_windrad` | Heizung, Speicher, Wasser, Windrad | `solarManager.py` |
| `skoda`, `skoda_raw` | Fahrzeugzustand im Verlauf | `gatherSkodaData.py` |
| `weatherStation`, `weatherHours`, `weatherDays`, `daylight`, `simPower` | Wetter, Sonnenzeiten, Ertragsprognose | Wetterbrücke, Open-Meteo, Prognose |
| `gridCosts`, `gasCosts`, `fuelCosts` | Preiszeitreihen (Stichtag, kein Enddatum) | Einstellungsseite (`costs.php`) |
| `kachelWerte` | Überschreibungen der Home-Kacheln, JSON je Raum | Einstellungsseite (`kacheln.php`) |
**`Logins`** — `users` (Passkeys) und `addUser` (Einmal-Links).
---
## Vier Bausteine im Detail
### 1. Die Home-Kacheln
Was auf einer Kachel steht, hat drei Stufen: die Vorgabe in `rooms.php`, die
Überschreibung in `kachelWerte`, und die Anzeige im Browser.
```mermaid
flowchart LR
ROOMS["rooms.php<br/><small>$rooms: Vorgabe je Raum</small>"] --> MIT["mitWerten()"]
KW[("kachelWerte<br/><small>je Raum, JSON</small>")] --> MIT
MIT --> RWT["roomsWithTile()"]
RWT -->|"als JSON in die Seite"| HOMEJS["homeMQTT.js<br/><small>homeRooms</small>"]
RWT --> SVG["home.php<br/><small>SVG-Kacheln</small>"]
TT["tileTopics()"] -->|"abonniert genau diese"| BROKER{{Broker}}
BROKER --> HOMEJS
HOMEJS -->|"kachelWert()"| SVG
SETJS["settings.js<br/><small>Reiter Kacheln</small>"] --> SETA["ajax/settings.php"]
SETA --> KACHM["kacheln.php<br/><small>prüfen, speichern</small>"] --> KW
KACHM -->|"Auswahlliste"| KAT["deviceCatalog()<br/><small>automations.php</small>"]
```
Eine Wertdefinition ist ein kleines JSON-Objekt, überall in derselben Form:
```json
{ "topic": "Power_UG/status/em:0", "pfad": "total_act_power",
"einheit": "W", "stellen": 0, "negativ": true,
"icon": "box-arrow-up", "gross": true }
```
`wechselrichter` + `feld` statt `topic` summiert mehrere Wechselrichter;
`format: "leistung"` skaliert selbst zwischen W und kW; `negativ` dreht das
Vorzeichen (für einen Zähler, der verkehrt herum eingebaut ist); `icon` ist
der Name **irgendeines** Bootstrap-Icons — die Codepunkte liest
`bootstrapIcons()` aus `css/bootstrap-icons.min.css`, gezeichnet wird als
SVG-Text in der Icon-Schrift.
Fehlt die Tabelle `kachelWerte`, zeigt jede Kachel ihre Vorgabe: die
Startseite ist keine Einstellung, ohne die man das Haus nicht mehr sieht.
### 2. Das Raum-Modal
```mermaid
sequenceDiagram
participant B as Browser
participant R as ajax/room.php
participant RC as roomControls.php
participant C as commands.php
participant G as Gerät
B->>R: GET ?room=EG_Bad
R->>RC: Geräte des Raums (actors.room) → Bedienformen
RC-->>B: fertiges Markup (Rollladen, Licht, Messwerte …)
B->>R: POST ?action=command {command_id, params}
R->>C: executeCommand()
alt mqtt://
C-->>G: Nutzlast auf das Topic
else wled://
C-->>G: JSON an /json/state
else http://
C-->>G: Abfrageargumente an die Geräte-URL
else Tahoma
C-->>G: exec/apply an die Box
end
```
`roomControls.php` entscheidet **an den Fähigkeiten**, nicht am Typnamen: Hat
das Gerät ein `setClosure`, wird es ein Rollladen mit Auf/Stop/Zu und zwei
Reglern; hat es drei Parameter rot/grün/blau, wird es eine Lampe mit
Farbwähler. Ein neu gefundenes Gerät bekommt so ohne Codeänderung ein
passendes Bedienelement.
`commands.php` ist das Gegenstück zu `transports.py` im SolarManager-Repo —
dieselbe Zuordnung „welche URL gehört zu welchem Weg“, einmal für den Runner,
einmal für den Browser.
### 3. Die Automatiken
Editor (`autoActionFuncs.js` + `AutoAction.php` + `automations.php`) schreibt
das Regelwerk nach `homeMesh`; ausgeführt wird es von
`autoActions/autoaction_runner.py` im anderen Repo. **Die Weboberfläche
schaltet nichts von sich aus** — sie beschreibt nur, was gelten soll.
**Drei Bänder statt eines Akkordeons: Wenn · Wann · Dann.** Das Akkordeon
schloss beim Öffnen die anderen Fächer — Auslöser und Aktion waren also nie
gleichzeitig zu sehen, ausgerechnet bei einer Regel, die genau daraus
besteht. Und das mittlere Fach hieß „Bedingungen", enthielt aber die
*Rahmen*bedingungen. Gebaut aus `.card` mit farbigem Rand und `.card-header`,
kein eigenes Bauteil.
**Der Rahmen steht zusammengefaltet da**, solange nichts von der Vorgabe
abweicht — und das ist der Normalfall. `rahmenKurzText()` schreibt ihn in
eine Zeile („täglich · rund um die Uhr"), `rahmenIstVorgabe()` entscheidet,
ob sie matt oder hell ist. Vorher nahm er die meiste Fläche für den
seltensten Inhalt.
**Neben jeder Bedingung läuft ihr aktueller Wert mit.** `?action=werte`
liefert alle acht Sekunden `{state_id: {value, unit}}` aus `actor_states`
bewusst aus der Tabelle und nicht über MQTT, weil dort nur ein Teil der
Geräte auftaucht (die Jalousien hängen an der Tahoma-Box, die gerechneten
Werte an gar nichts); der Runner schreibt dagegen jeden Messwert zurück.
Das ersetzt den Satz, der früher am Ende stand und die Regel Wort für Wort
nacherzählte: statt zu wiederholen, was darüber steht, beantwortet die Zeile
die Frage, die man wirklich hat — warum läuft die Automatik gerade nicht?
Ein voller Punkt heißt erfüllt, ein leerer noch nicht, ein **gestrichelter
heißt „entscheidet der Runner"**. Bei `time`, `date`, `datetime`,
`deltatime` und `elapsed` fällt der Browser bewusst kein Urteil: ob
„Uhrzeit um 07:30" gerade zutrifft, hängt am Nachholfenster, am Tagesrand
und beim Datentyp `elapsed` am echten Abstand seit der letzten Auslösung —
das alles steht in `bedingung_erfuellt()` im Runner. Es hier nachzubauen
hieße, eine zweite Wahrheit zu pflegen, die irgendwann ausei­nanderläuft.
Lieber keine Aussage als eine, die manchmal falsch ist (`ZEITARTEN`).
Bedingungen mit derselben `group_no` sind mit UND verknüpft, verschiedene
Gruppen mit ODER: ausgewertet wird `any(all(gruppe))`. Eine Bedingung zeigt
auf einen `actor_states`-Eintrag, eine Aktion auf ein `actor_commands` samt
Parametern. Deshalb dürfen Zustands-Ids nicht wandern — siehe die
Geräte-Erkennung im nächsten Abschnitt.
**Eine Automatik kann eine andere auslösen.** Dafür steht im Editor auffallend
wenig Code: das gerechnete Gerät „Automatiken" führt jede Automatik als
Messwert, ihr Wert ist der Zeitpunkt der letzten Auslösung — für den
Gerätewähler ist es damit ein Gerät wie jedes andere. Die Messwerte legt der
Runner an und pflegt sie, `restricted/automations.php` liest sie nur
(`ausloeserStates()`, `automationTriggers()`).
Drei Stellen wissen trotzdem davon:
* `pruefeKreis()` lehnt beim Speichern ab, was sich mittelbar selbst auslösen
würde. Im Betrieb wäre ein Kreis kaum zu bemerken — die Sperrzeit begrenzt
ihn auf eine Auslösung je `lockout_secs`, und im Protokoll sieht das aus wie
eine Automatik, die halt oft läuft.
* `deleteAutomation()` fragt vorher. `fk_cond_state` steht auf
`ON DELETE CASCADE`, mit dem Messwert verschwände also still die Bedingung
des Nachfolgers: aus *zehn Minuten nach dem Wecker UND es ist hell* würde
ein bloßes *es ist hell*. Zur Wahl stehen Abhängen (Bedingung raus,
Nachfolger pausiert) und Mitlöschen.
* Die Übersicht rückt Nachfolger unter ihren Auslöser ein und zeigt sie
eingeklappt (`toggleAutomationGroup()`). Das ist **nur die Darstellung**
jeder Nachfolger bleibt eine vollwertige Automatik mit eigener Pause,
eigenen Rahmenbedingungen und eigener Zeile im Protokoll. Sobald
„gruppiert" auch „gehört dazu" hieße, wäre zu klären, wem die Pause gehört.
**Die Tagesauswahl in vier Stufen, von grob nach fein.** Ganz oben drei
Vorlagen (`tagVorlagen()`): *täglich*, *werktags*, *Wochenende*. Sie setzen den
**ganzen** Rahmen, nicht nur die Wochentagsmaske — „werktags" heißt auch
„nicht an Feiertagen", „Wochenende" auch „dazu alle Feiertage". Wer das aus
Maske und zwei Schaltern von Hand zusammensetzt, vergisst den zweiten Teil.
Darunter sieben Wochentagsschalter, darunter Ferien und Feiertage mit je drei
Stufen — **nie · egal · immer** (`kalendertagChoices()`) —, und ganz unten ein
Satz, der zurückliest, was dabei herausgekommen ist: *„Läuft samstags und
sonntags, dazu an allen Feiertagen."*
Der Satz ist der eigentliche Gewinn: man muss das Bedienelement nicht
entziffern. `rahmenText()` in `autoActionFuncs.js` baut ihn, zieht drei und
mehr aufeinanderfolgende Tage zu „montags bis freitags" zusammen und gibt bei
gar keinem wählbaren Tag `""` zurück — dann steht dort die Warnung *„Kein Tag
ausgewählt"*, genau wie beim fehlenden Auslöser.
„Immer" zählt wie ein angehakter Wochentag. Das ist die einzige Art, *an
Wochenenden und Feiertagen* zu schreiben — für die Maske ist ein Feiertag am
Dienstag eben ein Dienstag —, und mit **gar keinem** Wochentag angehakt ergibt
es *nur an Feiertagen*.
Vorher standen dort zwei Auswahlfelder, deren Einträge ganze Sätze waren
(„zusätzlich, auch am falschen Wochentag"). Ein Auswahlfeld zeigt immer nur
den gewählten Eintrag — man sieht nie, was es sonst noch gibt, und die
Erklärung steht an der Stelle, wo ein Etikett hingehört.
`kalendertagWert()` prüft beim Speichern mit `is_numeric`, weil `intval("")`
sonst 0 wäre — und 0 heißt „nie": eine leere Angabe hätte eine Automatik
stillschweigend an Feiertagen abgeschaltet.
**„Nur einmal am Tag"** (`once_per_day`) sperrt eine Automatik nach dem
Auslösen bis Mitternacht. Für alles, was man hinterher von Hand wieder anders
stellt: ein Rollladen, den man um acht zugezogen hat, soll nicht um neun von
selbst wieder auffahren, nur weil eine Wolke weiterzieht.
Wie der Runner das auswertet — Datentyp `elapsed`, topologische Reihenfolge,
der Rahmen, was beim Pausieren geschieht — steht in
[`autoActions/README.md`](../../homes/wagner/SolarManager/autoActions/README.md)
im SolarManager-Repo.
### 4. Die Geräte-Erkennung
`restricted/deviceDiscovery/device_discovery.py` wird **von Hand** gestartet.
Die Module suchen je eine Geräteart (Tahoma, WLED, Shelly, MQTT/Home-Assistant-
Discovery, Gartenwasser, SolarManager-Werte, Wetterstation, Logic); die
Datenbanklogik liegt allein im Hauptskript.
Drei Eigenschaften, die man kennen muss:
* **Ein Suchlauf löscht nichts.** `clear_tables` steht in `config.ini` auf
`false`, weil die Automatiken auf die Ids von `actor_states` zeigen.
Bestehende Zeilen werden über den Schlüssel `(actor_id, state_name)`
aktualisiert, neue kommen dazu. Abgebaute Geräte verschwinden nur über
**Einstellungen → Geräte → Papierkorb** (und auch dort nur, wenn keine
Automatik mehr auf sie zeigt).
* **Shelly, zwei Wege — und zwar je Komponente.** Alte Shellys können kein
MQTT und bleiben ganz beim HTTP-Weg: in `actor_states.url` steht ein
Feldname der JSON-Antwort. Bei Gen2-Geräten mit eingeschaltetem `status_ntf`
steht dagegen das Topic in `url` (`Power_EG/status/em:0`) und der Schlüssel
in `value_path`.
Das gilt aber nur für Komponenten, die auch wirklich senden — die Liste
steht als `MQTT_KOMPONENTEN` in `modules/shelly_module.py` und enthält
heute `em` und `switch`. Der Pro 3EM etwa hat eine Temperaturkomponente im
RPC-Status, veröffentlicht sie aber nie; ein Messwert auf diesem Topic
bliebe für immer auf seinem letzten Wert stehen. Alles außerhalb der Liste
wird deshalb weiter abgefragt. Wer eine Komponente ergänzen will, hört
vorher mit (`mosquitto_sub -t '<präfix>/#' -v`).
Geschaltet werden auch die neuen Geräte über HTTP. Der Runner teilt seine
Messwerte deshalb je Wert zu, nicht je Gerät.
* **Vier Module suchen gar nichts.** Gartenwasser, SolarManager, Wetterstation
und Logic schreiben feste Geräte hin, weil sich diese Quellen nicht von
selbst melden. Wer einen Messwert vermisst, trägt im jeweiligen Modul eine
Zeile ein — bei der Wetterstation in `MESSWERTE`, beim SolarManager in
`GERAETE`. Bewusst knapp gehalten: `solarManager/#` allein trägt gut 130
Topics, und eine vollständige Liste machte die Auswahl im Automatik-Editor
unbrauchbar.
Die **Wetterstation** hängt am Websocket von `192.168.179.42`;
`wsMQTTbridge.py` im SolarManager legt ihre Felder unter `weatherStation/#`
ab. Von dort kommen Außentemperatur, Wind und Böe — die Außentemperatur ist
für den Hitzeschutz die bessere Bedingung als die Raumtemperatur (ist es
drinnen schon warm, ist es zum Verschatten zu spät), und ohne den Wind gibt
es für Markise und Sonnensegel keinen Sturmschutz. Die trägen Werte
(Temperatur, Feuchte, Druck, Taupunkt) gehen nur bei jeder einundzwanzigsten
Nachricht raus; sie sind retained, für eine Bedingung macht das keinen
Unterschied.
---
## Wiederkehrende Bauteile und Konventionen
**Zwei Radien, eine Anzeigeschrift.** `solar.css` tauscht die Palette über
Variablen aus, statt Regel für Regel zu überschreiben — dasselbe gilt für
Form und Schrift. `--ton-radius` (`.875rem`) gilt für Flächen, `--ton-radius-klein`
(`.5rem`) für alles, was in einer Reihe steht; beide hängen an
`--bs-border-radius` und `--bs-border-radius-sm`, also zieht jede Karte,
jedes Modal und jeder kleine Knopf von allein mit. `--font-anzeige` ist
Poppins — die Schrift lag seit jeher unter `assets/fonts` und trug nur die
vier Etagenknöpfe im Außenplan; jetzt trägt sie alle Überschriften,
`.card-title` und `.modal-title`. Fließtext und Zahlenkolonnen bleiben
bewusst bei der Schrift des Themes: Poppins bringt keine Tabellenziffern
mit, Messwerte würden sonst springen.
**Drei Klassen aus dem Automatik-Editor stehen absichtlich in `solar.css`
und nicht dort.** `.feld-als-text` ist ein Auswahlfeld, das wie Text
aussieht, bis man es anfährt — dieselbe Not haben das Raum-Modal und die
Kachel-Einstellungen, wo ebenfalls jede Kleinigkeit in einem Kasten steht.
`.punkt` ist an oder aus. `.wert-jetzt` ist die eine echte Ausnahme —
Bedingungen gibt es sonst nirgends — und als einzelne Klasse auch als solche
erkennbar.
**Reiter.** Es gibt genau eine Bauart, `restricted/reiter.php`. Umgeschaltet
wird von Bootstrap (`data-bs-toggle="tab"`), gemerkt wird der offene Reiter von
`reiterMerken()` in `common.js`. Weniger als zwei Reiter ergeben keine Leiste.
**Stylesheet-Reihenfolge — die häufigste Falle.** `header.php` lädt
`css/solar.css` **vor** `css/adminlte.min.css`. Eine eigene Regel mit gleicher
Spezifität wie eine AdminLTE-Regel verliert deshalb. Wenn eine Änderung
„nicht wirkt“, ist das fast immer der Grund: Selektor verlängern
(`.nav-tabs.reiter .nav-link` statt `.reiter .nav-link`), nicht `!important`.
**Zwischenspeicher.** Jedes eigene Skript und `solar.css` werden mit
`?v=<filemtime>` eingebunden — gespeichert heißt geladen.
**Sprache.** Bezeichner und Kommentare sind deutsch, wo es um die Sache geht;
englisch bleibt, was von außen vorgegeben ist (`floor`, `topic`, Chart.js-
Optionen, Spaltennamen). Kommentare erklären **warum**, nicht was.
**Fehlertoleranz vor Vollständigkeit.** Was eine Seite nicht zwingend braucht,
wird in `try/catch` geholt: fehlt die Mesh-Datenbank, bleibt die Startseite
sichtbar; fehlt `kachelWerte`, gilt die Vorgabe.
**Zugriffsschutz.** `.htaccess` sperrt Punkt-Dateien (allen voran `.git`) und
alle Dateitypen, die der Browser nie anfordert (`.py`, `.sql`, `.ini`, `.md`
…). `restricted/mysql.php` steht in `.gitignore`; Vorlage ist
`mysql.php.example`. Zugangsdaten gehören nirgendwo sonst hin.
**Zeilenenden.** `.gitattributes` erzwingt LF — der Ordner wird von Linux
ausgeliefert.
---
## Wo fange ich an, wenn ich … ändern will
| Vorhaben | Anlaufstelle |
|---|---|
| Ein Raum kommt dazu oder heißt anders | `restricted/rooms.php`, Tabelle `$rooms` — Kachel, Menü und MQTT-Abo ergeben sich daraus |
| Andere Werte auf einer Kachel | Einstellungen → Kacheln (keine Codeänderung). Vorgabe für neue Räume: `kachelVorgabe()` in `rooms.php` |
| Neues Symbol auf einer Kachel | Name eines Bootstrap-Icons ins Feld tippen. Vorschlagsliste: `kachelSymbole()` in `kacheln.php` |
| Neue Kennzahl in der Jahresstatistik | `KENNZAHLEN` **und** `kennzahlenAus()` in `ajax/getStats.php` — sonst nichts |
| Neues Diagramm auf einer Seite | Karte in der Vorlage, `<canvas>`, 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 |
| 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.
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, Runner, Wecker |
Rekursive Suchen (`grep -r`) laufen über die SMB-Freigabe in Zeitüberschreitungen —
auf der NAS selbst sind sie sofort fertig.