Raum-Modal, Zeitleiste und Automatik-Editor zeigten drei verschiedene Zeichen; bi-blinds gibt es in der Icon-Schrift gar nicht. Das neue Symbol liegt als CSS-Maske in solar.css und verhaelt sich wie ein Icon aus der Schrift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
954 lines
52 KiB
Markdown
954 lines
52 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.
|
||
|
||
---
|
||
|
||
## 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` | `settings.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.
|
||
|
||
**`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.
|
||
|
||
**`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`) |
|
||
| 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.
|