doku/byd.md: warum der Umweg über die BMU überhaupt nötig ist (der Gen24 kennt weder Zellen noch SOH noch die Zähler), der Ablauf der sechs Anfragen in einer einzigen Verbindung, und vor allem die Eigenheit, aus der sich der ganze Aufbau des Sammlers ergibt — das Netzwerkmodul startet alle ~102 Sekunden neu und bedient danach nur die ersten ein, zwei Verbindungen. Dazu die Byte-Lagen samt der einen Abweichung vom ioBroker-Adapter (wortvertauschte Gesamtzähler), die Aufteilung der 128 Zellen auf vier Antworten, was unter solarManager/byd/… herauskommt, die beiden Tabellen in solarLog und wie speicher.js daraus die Ansicht baut. Dabei aufgefallen und vermerkt: ajax/speicher.php?was=gesundheit hat keinen Aufrufer. Die Tageswerte für ein Alterungsdiagramm liegen also schon bereit, es fehlt nur die Anzeige. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
975 lines
54 KiB
Markdown
975 lines
54 KiB
Markdown
# 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)**,
|
||
**[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,<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`, `anlageKatalog.js`, `anlage.js`, `speicher.js`, `energieflussKatalog.js`, `energiefluss.js`, `bewaesserungAnzeige.js`, `ladefenster.js`, `bedienfenster.js`, `solarMQTT.js` | `speicher`, `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 — das Meteogramm ist ein fremdes Dokument im `<iframe>` (`js/meteogram.js`) | `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<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
|
||
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=<Raumnummer>`; 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 `<select>` um einen eigenen
|
||
Knopf; das `<select>` bleibt stehen und hält weiter den Wert, vorhandene
|
||
`onchange`-Zuhörer merken nichts davon. Die aufgeklappte Liste hängt an
|
||
`<body>` 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 (`<optgroup>`) bleiben Gruppen, die Tastatur bedient sie.
|
||
Neue `<select>` übernimmt ein `MutationObserver` von selbst. Wer den Wert im
|
||
Code setzt, ruft `auswahlAktualisieren(select)` oder löst `change` aus;
|
||
`data-auswahl="aus"` lässt ein Feld in Ruhe.
|
||
|
||
**`solarMQTT.js`** — die Solar-Seite. Startet die Energiefluss-Übersicht
|
||
(`energieflussStarten()`) samt Anlage-Ansicht, füllt die Bewässerungszonen,
|
||
darunter hängen die Diagramme Prognose, Verbrauch und Erzeugung über
|
||
`getData()`. Aktualisiert wird höchstens einmal je Sekunde
|
||
(`solarSvgUpdatePending`); die Setter (`setText`, `setAttr`) schreiben nur,
|
||
was sich geändert hat.
|
||
|
||
**Energiefluss-Übersicht** — drei Dateien mit klarer Arbeitsteilung:
|
||
|
||
| Datei | enthält |
|
||
|---|---|
|
||
| `energieflussKatalog.js` | **jeden Kreis genau einmal**: Name, Symbol, Farbe, Elternkreis, Lage, Ringskala, Klickziel und `lesen(m, opt)`, das aus den MQTT-Werten Leistung, Zeile darunter, Ringsegmente, Fluss, Plaketten und Symbolzustand macht. Ein neuer Kreis ist ein neuer Eintrag hier — sonst nichts. |
|
||
| `energiefluss.js` | den Renderer (`new Energiefluss(behälter, opt)`): Symbole (24er-Raster, animierbare Teile), Kreise mit Ring und Füllstandswelle, Flüsse als Bögen mit laufenden Punkten, Plaketten, Hochkant-Ansicht (x/y vertauscht unter 720 px Breite), Bearbeitungsmodus mit Ziehen. Kennt keinen Kreis beim Namen. |
|
||
| `energieflussEinstellungen.js` | Reiter „Übersicht“ in den Einstellungen: Vorschau mit Beispielwerten, Kreise an/aus, Name, Lage per Ziehen, PV-Flächen (welche Einträge von `P_PVn` zu welcher Fläche gehören). |
|
||
|
||
Was ein Haus vom Katalog abweicht, steht in `homeMesh.energiefluss`
|
||
(`homeMesh_energiefluss.sql`, gelesen/geschrieben in `restricted/energiefluss.php`) —
|
||
nur echte Abweichungen, eine leere Tabelle ist der Katalog. Ist ein Kreis aus,
|
||
hängen seine Kinder am nächsten eingeschalteten darüber, und sein Anteil im
|
||
Ring des Elternkreises fällt in das letzte Segment. Die Animationen stehen
|
||
still, solange die Karte nicht zu sehen ist, und entfallen bei
|
||
`prefers-reduced-motion`. Stile: Abschnitt „Energiefluss-Übersicht“ in
|
||
`css/solar.css`. Bis September 2026 stand hier `assets/img/realtime.svg` mit
|
||
festen Kreisen.
|
||
|
||
**Anlage-Ansicht** (Umschalter „Anlage“ oder Klick auf die Sonne) — dieselbe
|
||
Arbeitsteilung:
|
||
|
||
| Datei | enthält |
|
||
|---|---|
|
||
| `anlageKatalog.js` | die PV-Anlage dieses Hauses: Flächen mit Umriss, Wechselrichter (`invertersN` und ihre Eingänge in `P_PVn`), Strings ohne Einzelwerte (Gen24) und jede Platte mit ihrer echten Lage. Auch die Flächen im Sonnenring der Übersicht leiten sich daraus ab. |
|
||
| `anlage.js` | den Renderer (`new Anlage(behälter)`): Flächen mit Leistungsanteil, Lageplan (hochkant gedreht unter 800 px), Detail zu Platte, String oder Wechselrichter, Wechselrichterliste mit Auslastung und Zustand. |
|
||
|
||
Die Farbe einer Platte zeigt ihre Leistung im Vergleich zu den stärksten
|
||
ihrer Fläche (90. Perzentil) — nur Anzeige, kein Urteil. Ein Warnzeichen
|
||
bekommt sie erst, wenn sie bei genug Licht (Flächenmedian ab 30 W) unter 70 %
|
||
des Medians liegt. Meldet ein Wechselrichter nichts, stehen seine Platten auf
|
||
„ohne Daten“ statt „auffällig“. Den Verlauf je Platte gibt es nicht in der
|
||
Datenbank; die Ansicht schreibt ihn ab dem Seitenaufruf mit (40 Minuten).
|
||
|
||
**Speicher-Ansicht** (Umschalter „Speicher“ oder Klick auf die Batterie) —
|
||
`speicher.js` zeigt die Batterie bis zur Zelle: Kennzahlen (Ladestand mit kWh
|
||
und „voll/leer um“, Leistung, SOH, Zellspreizung, Temperaturen, Spannung,
|
||
Durchsatz mit gerechneten Zyklen, Zustand), den Turm aus BMU und Modulen mit
|
||
je 16 Zellbalken (Farbe und Höhe = Abweichung vom Mittel aller Zellen), das
|
||
gewählte Modul mit Zellspannungen und Temperaturfühlern sowie zwei
|
||
Verläufe aus `ajax/speicher.php` (Ladestand und Leistung aus `EnergyFlow`,
|
||
Spreizung und Temperatur aus `byd`). Live-Werte kommen über MQTT
|
||
`solarManager/byd/#` (`gatherBYDData.py`); fehlen sie, zeigt die Ansicht, was
|
||
der Wechselrichter weiß. Die Nennenergie je Modul (2,76 kWh, HVM) steht als
|
||
Konstante oben in `speicher.js` — die BMU meldet sie nicht. Wie die Werte
|
||
aus der BMU herauskommen, steht in **[doku/byd.md](doku/byd.md)**.
|
||
|
||
**`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`).
|
||
|
||
**`zeitleiste.js`** — die Automatismen als Zeitleiste eines Tages, neben der
|
||
bisherigen Liste (Umschalter oben in der Karte, gemerkt im Browser). Eine Bahn
|
||
je Geräteart, abgeleitet aus den geschalteten Geräten über `bedienform()` — die
|
||
Mehrheit gewinnt, Bewässerung wird am Gerätetyp erkannt. Die Lage rechnet
|
||
`restricted/zeitleiste.php` nach festen Regeln: feste Uhrzeit und Sonnenstand
|
||
als Punkt, Zeitfenster mit Messwert als Balken, Ketten hinter ihrem Auslöser,
|
||
ohne jede Zeit unter „Jederzeit“, pausierte unter „Pausiert“. Ob eine
|
||
Automatik an einem Tag dran ist, wird wie im Runner gerechnet (Wochentage,
|
||
Ferien, Feiertage, Vorabend); nicht dran heißt gestrichelt, nie ausgeblendet.
|
||
Der Zähler „x von y“ macht sichtbar, dass keine fehlt. Etagen lassen sich
|
||
einzeln einblenden. Sonnenzeiten kommen aus `solarLog.daylight`, für spätere
|
||
Tage vom selben Kalendertag eines Vorjahres.
|
||
|
||
**`grundriss.js`** — der Reiter Grundriss: Etagen, Räume, Grundrissbilder und
|
||
der Plan-Editor, in dem Kacheln gezogen und gesetzt werden. Eigene Datei, weil
|
||
`settings.js` schon für vier Reiter reicht; es nutzt `textSchuetzen()` von dort.
|
||
|
||
**`settings.js`** — vier Reiter in einer Datei, deutlich getrennt: Preistabellen,
|
||
Home-Kacheln, Geräte, Fahrzeug. Jeder Abschnitt hat sein Modell (`kachelModell`,
|
||
`geraeteModell`, `skodaModell`), 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=<rooms.id>`, `POST ?action=command\|temp\|jalousie\|my\|wled` | Raum-Modal bauen; ein Kommando oder eine Solltemperatur senden; Jalousie-Stand und WLED-Zustand (Effekt, Preset) live lesen; aktuellen Stand als my-Position in den Motor schreiben (`setMemorized1Position`/`-Orientation`) |
|
||
| `AutoAction.php` | `?action=editor\|list\|zeitleiste\|followers\|werte`, `POST save\|delete\|toggle` | Automatik-Editor, Übersicht als Liste und als Zeitleiste (`&datum=YYYY-MM-DD`) |
|
||
| `settings.php` | `?action=list\|kacheln\|geraete\|grundriss\|skoda-keys`, `POST save\|kacheln-save\|geraete-raeume\|geraet-loeschen\|etage-speichern\|etagen-reihenfolge\|etage-loeschen\|etage-bild\|raum-speichern\|raum-position\|raum-loeschen\|skoda-keys-save` | Einstellungsseite; die Grundriss-Aktionen antworten mit dem ganzen neuen Stand, `skoda-keys` meldet nur, ob ein Schlüssel hinterlegt ist und worauf er endet |
|
||
| `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` `carForm.php` `heater.php` `watering.php` `tahoma.php` | | Wallboxen, Heizstab, Bewässerung, Tahoma-Bedienung. Das Ladefenster (`carForm.php` + `js/solar/ladefenster.js`) liest live aus MQTT und schreibt jede Änderung sofort als JSON an `carSteuerung.php` (Aktionen `frc`, `modus`, `strom`, `plan`). Heizstab- und Bewässerungsfenster (`heater.php`, `watering.php` + `js/solar/bedienfenster.js`) arbeiten genauso: Zustand live aus MQTT, jede Wahl gilt sofort |
|
||
| `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` | Etagen und Räume lesen (`homeMesh`), Kachelvorgabe, Symbole | `grundriss()`, `allRooms()`, `roomsOnFloor()`, `floorsWithPlan()`, `standardEtage()`, `etageBild()`, `roomsWithTile()`, `tileTopics()`, `kachelVorgabe()`, `bootstrapIcons()`, `kachelIcon()` |
|
||
| `zeitleiste.php` | Automatiken eines Tages für die Zeitleiste | `zeitleiste()`, `zlTagPasst()`, `zlSonne()`, `zlBahn()` |
|
||
| `grundriss.php` | Etagen, Räume, Kachelpositionen und Grundrissbilder ändern | `grundrissStand()`, `etageSpeichern()`, `etagenReihenfolge()`, `etageLoeschen()`, `etageBildSpeichern()`, `raumSpeichern()`, `raumPosition()`, `raumLoeschen()` |
|
||
| `meshdb.php` | Verbindung zu `homeMesh` | `meshDb()` |
|
||
| `costs.php` | Preistabellen, Verbindung zu `solarLog` | `solarDb()`, `preiseLaden()`, `preiseSpeichern()`, `grundpreisImZeitraum()` |
|
||
| `automations.php` | Automatiken, Gerätekatalog, Raumzuordnung der Geräte | `deviceCatalog()`, `raumListe()`, `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()` |
|
||
| `skodaKeys.php` | MyŠkoda-Schlüssel in `skoda.conf` (SolarManager): lesen, prüfen, eintragen — ein Wert geht nie an den Browser zurück | `skodaStand()`, `skodaSchluesselSpeichern()` |
|
||
| `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>"]
|
||
COSTS["costs.php"] --> MYSQL
|
||
MESH["meshdb.php<br/><small>meshDb()</small>"] --> MYSQL
|
||
ROOMS["rooms.php"] --> MESH
|
||
GRM["grundriss.php"] --> ROOMS
|
||
KACH["kacheln.php"] --> ROOMS
|
||
KACH -. "nur wenn vorhanden" .-> AUTOM["automations.php"]
|
||
AUTOM --> MESH
|
||
AUTOM --> ROOMS
|
||
CMD["commands.php"] --> MYSQL
|
||
CMD --> TAH["tahoma_EG.php"]
|
||
CMD --> PHPMQTT["ajax/phpMQTT.php"]
|
||
HOME["home.php"] --> ROOMS
|
||
HOME --> REITER
|
||
SET["settings.php"] --> COSTS & KACH & REITER
|
||
SET --> SKK["skodaKeys.php<br/><small>skoda.conf</small>"]
|
||
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` |
|
||
| `skoda_ladepunkte` | Wallbox-Verlauf während einer Ladung, im Minutentakt, wird nicht ausgedünnt (`solarLog_skoda_ladepunkte.sql`) | `gatherSkodaData.py`; ältere Ladungen `skoda_ladepunkte_nachtragen.py` |
|
||
| `byd`, `byd_zellen` | BYD-Speicher direkt aus der BMU: Ladestand, SOH, Temperaturen, Spreizung, Zähler alle 5 Minuten; alle 128 Zellspannungen und 64 Temperaturen alle 15 Minuten (`solarLog_byd.sql`) | `gatherBYDData.py` |
|
||
| `weatherStation`, `weatherHours`, `weatherDays`, `daylight`, `simPower` | Wetter, Sonnenzeiten, Ertragsprognose | Wetterbrücke, Open-Meteo, Prognose |
|
||
| `gridCosts`, `gasCosts`, `fuelCosts` | Preiszeitreihen (Stichtag, kein Enddatum) | Einstellungsseite (`costs.php`) |
|
||
|
||
**`Logins`** — `users` (Passkeys) und `addUser` (Einmal-Links).
|
||
|
||
---
|
||
|
||
## Vier Bausteine im Detail
|
||
|
||
### 1. Grundriss und Home-Kacheln
|
||
|
||
Etagen, Räume, Kachelpositionen und Kachelwerte sind **Inhalt, kein Code**.
|
||
Sie liegen in `homeMesh.floors` und `homeMesh.rooms` (`homeMesh_grundriss.sql`)
|
||
und werden unter **Einstellungen → Grundriss** und **→ Home-Kacheln** gepflegt.
|
||
Bis September 2026 standen sie als Tabelle in `rooms.php`; für ein anderes
|
||
Haus hätte man den Quelltext umschreiben müssen.
|
||
|
||
| Tabelle | trägt |
|
||
|---|---|
|
||
| `floors` | `code` (fest: URL, SVG-Ids, `automations.floor`), `label`, Reihenfolge, Grundrissbild, Standard-Etage |
|
||
| `rooms` | feste Nummer `id`, Etage, Name, `kuerzel` (SVG-Ids), Kachel `x`/`y` (leer = keine Kachel), `thermostat` (MQTT-Zweig), `werte` (JSON, leer = Vorgabe) |
|
||
| `energiefluss` | Abweichungen der Solar-Übersicht vom Katalog: `aktiv`, `name`, `x`/`y`, `optionen` (JSON) — eigene Datei `homeMesh_energiefluss.sql`, keine Zeile = Katalog |
|
||
|
||
Auf die feste Raumnummer zeigen `actors.room_id` und das Raum-Modal
|
||
(`room.php?room=12`). Ein umbenannter Raum behält damit Geräte, Regler und
|
||
Kachel. `automations.floor` ist ein Fremdschlüssel auf `floors`: eine Etage mit
|
||
Automatiken lässt sich nicht löschen.
|
||
|
||
**Die Vorgabe einer Kachel** ist allein das Thermostat: mit `thermostat` Soll,
|
||
Ist und Feuchte aus diesem Zweig, sonst nur der Name. Derselbe Zweig trägt
|
||
Heizsymbol und Temperaturregler (`mqttZweig()` in `homeMQTT.js`,
|
||
`<zweig>/changeSetTemp` in `room.php`) — nichts setzt mehr voraus, dass ein
|
||
Thermostat unter `Raumtemp/<Etage>/<Raum>` meldet.
|
||
|
||
**Grundrissbilder** kommen per Upload nach `tiles/grundriss/` — das einzige
|
||
Verzeichnis, in das der Webserver schreiben darf, und nicht im Git. PNG, JPEG
|
||
oder WebP, geprüft am Inhalt; SVG bewusst nicht, es könnte Skript enthalten.
|
||
Der Name trägt einen Teil der Prüfsumme, ein neues Bild hat also eine neue
|
||
Adresse. Die mitgelieferten unter `assets/img` bleiben daneben gültig.
|
||
|
||
**Der Plan-Editor** (`js/solar/grundriss.js`, Modell `restricted/grundriss.php`)
|
||
löst `tools/kachelpositionen.php` ab: Kacheln werden gezogen, ein Raum ohne
|
||
Kachel mit „Platzieren“ und einem Klick gesetzt. Übernommen sind Raster,
|
||
Geisterkachel und die Umrechnung von der gezeigten Mitte zur gespeicherten
|
||
linken oberen Ecke. Jede Aktion antwortet mit dem ganzen neuen Stand. Nach
|
||
einer Änderung laden „Home-Kacheln“ und „Geräte“ beim nächsten Öffnen nach.
|
||
|
||
Ohne Etage mit platziertem Raum zeigt die Startseite statt der Zeichenfläche
|
||
den Weg zu den Einstellungen — der Anfang jeder neuen Installation.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
DB[("homeMesh.floors<br/>homeMesh.rooms")] --> GR["grundriss()<br/><small>rooms.php</small>"]
|
||
GR --> MIT["mitWerten()"]
|
||
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
|
||
|
||
GRJS["grundriss.js<br/><small>Reiter Grundriss</small>"] --> SETA["ajax/settings.php"]
|
||
SETJS["settings.js<br/><small>Reiter Kacheln</small>"] --> SETA
|
||
SETA --> GRM["grundriss.php<br/><small>Etagen, Räume, Bilder</small>"] --> DB
|
||
SETA --> KACHM["kacheln.php<br/><small>Werte prüfen, speichern</small>"] --> DB
|
||
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 (`"leistung_kw"`, wenn
|
||
die Quelle schon Kilowatt schickt); `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.
|
||
|
||
|
||
### 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=12 (rooms.id)
|
||
R->>RC: Geräte des Raums (actors.room_id) → 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
|
||
Höhenstufen, mit `my` kommt der my-Knopf dazu, mit Neigung die Lamellenstufen;
|
||
hat es drei Parameter rot/grün/blau, wird es eine Lampe mit Farbfeldern. 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 auseinanderlä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.
|
||
|
||
**„Am Vorabend"** (`next_day`) lässt Wochentage, Ferien und Feiertage für
|
||
**morgen** gelten. Für alles, was abends für den nächsten Tag geschieht:
|
||
*Kinderrollos zu, wenn morgen Schule ist* heißt dann Mo–Fr, Ferien nie,
|
||
Feiertage nie — dieselben Tage wie der Wecker. `rahmenText()`,
|
||
`rahmenKurzText()` und die Spalte „Wann" der Übersicht nennen den Vorabend
|
||
vorne, damit auch „nicht an Feiertagen" als Folgetag gelesen wird. Uhrzeit
|
||
und Zeitfenster bleiben beim heutigen Tag.
|
||
|
||
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 oder der Grundriss, zeigt
|
||
die Startseite einen Hinweis statt eines Fatal Errors.
|
||
|
||
**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 oder eine Etage kommt dazu, heißt anders, bekommt ein neues Grundrissbild | Einstellungen → Grundriss (keine Codeänderung) — Kachel, Menü und MQTT-Abo ergeben sich daraus |
|
||
| Andere Werte auf einer Kachel | Einstellungen → Home-Kacheln (keine Codeänderung). Vorgabe ohne eigene Werte: `kachelVorgabe()` in `rooms.php` |
|
||
| Ein anderes Haus einrichten | `homeMesh_DB-layout.sql`, `homeMesh_automations.sql`, `homeMesh_grundriss.sql`, `homeMesh_energiefluss.sql` in dieser Reihenfolge; dann Einstellungen → Grundriss und → Übersicht |
|
||
| Kreis der Energiefluss-Übersicht aus, umbenennen, verschieben; PV-Flächen | Einstellungen → Übersicht (keine Codeänderung) |
|
||
| Neuer Kreis in der Energiefluss-Übersicht (Gerät, Etage, Quelle) | ein Eintrag in `js/solar/energieflussKatalog.js`; neues Symbol dazu in `SYMBOLE` (`energiefluss.js`) |
|
||
| Auswahlliste irgendwo | ein gewöhnliches `<select>` 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, `<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. 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.
|