diff --git a/README.md b/README.md index 72596fc..7b41695 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,12 @@ Dieses Verzeichnis ist zugleich der Web-Root der NAS Der Arbeitsordner *ist* die laufende Anwendung — jede gespeicherte Datei ist sofort live. -Für zwei Teile reicht diese Landkarte nicht, weil ihr Verhalten aus dem -Zusammenspiel von Editor, Datenbank und Runner entsteht. Sie haben eigene, -ausführliche Dokumente unter [`doku/`](doku/README.md): -**[Automatiken](doku/automatiken.md)** und -**[Zeitleiste](doku/zeitleiste.md)**. +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)**, +**[Datenbanken](doku/datenbank.md)** und +**[Einstellungsseite](doku/einstellungen.md)**. --- diff --git a/doku/README.md b/doku/README.md index 64c7690..008fdff 100644 --- a/doku/README.md +++ b/doku/README.md @@ -13,6 +13,8 @@ abzulesen sind. |---|---| | [automatiken.md](automatiken.md) | Die Automatiken (AutoActions): Datenmodell, Editor, Runner, wie eine Bedingung wirklich ausgewertet wird, Verkettung, Sperren, Fehlerbilder | | [zeitleiste.md](zeitleiste.md) | Die Zeitleiste der Automatiken: wie der Server den Tag ausrechnet und wie der Browser ihn zeichnet | +| [datenbank.md](datenbank.md) | Die drei Schemata: wer was schreibt, wie Messreihen verdichtet werden, welche Tabelle man für welche Auswertung nimmt | +| [einstellungen.md](einstellungen.md) | Die Einstellungsseite: was jeder Reiter speichert, welche Regeln das Modell durchsetzt und was die Seite bewusst nicht kann | --- diff --git a/doku/datenbank.md b/doku/datenbank.md new file mode 100644 index 0000000..7d33d17 --- /dev/null +++ b/doku/datenbank.md @@ -0,0 +1,185 @@ +# Die Datenbanken + +Drei Schemata auf derselben MariaDB der NAS (Port 3310). Die Trennung ist +keine Ordnungsliebe, sondern Zuständigkeit: + +| Schema | Inhalt | Kennzeichen | +|---|---|---| +| **`homeMesh`** | Geräte, Automatiken, Grundriss — **was gilt** | klein, viele Fremdschlüssel, wird von Hand gepflegt | +| **`solarLog`** | Messreihen, Statistik, Preise — **was war** | groß, schreibt fast nur die NAS, wird verdichtet | +| **`Logins`** | Passkeys und Einmal-Links | winzig, sicherheitsrelevant | + +Wer von wo verbindet: + +| Aufrufer | Funktion / Datei | Zugang | +|---|---|---| +| Weboberfläche | `meshDb()` (`restricted/meshdb.php`) | `homeMesh` | +| Weboberfläche | `solarDb()` (`restricted/costs.php`) | `solarLog` | +| Weboberfläche | `commandDb()` (`restricted/commands.php`) | `homeMesh`, für Kommandos | +| Weboberfläche | `checkLogin()` (`helper.php`) | `Logins` | +| NAS-Prozesse | `config.ini` im SolarManager | beide, eigene Benutzer | + +Die Zugangsdaten stehen in `restricted/mysql.php` (Web) bzw. `config.ini` +(NAS) — beides nicht im Git. + +--- + +## 1. `homeMesh` — was gilt + +```mermaid +erDiagram + floors ||--o{ rooms : "hat" + rooms ||--o{ actors : "room_id" + actors ||--o{ actor_states : "meldet" + actors ||--o{ actor_commands : "kann" + actor_commands ||--o{ command_parameters : "nimmt" + state_types ||--o{ actor_states : "Datentyp" + + automations ||--o{ automation_conditions : "wenn" + automations ||--o{ automation_actions : "dann" + automations ||--o{ automation_log : "Protokoll" + automation_conditions }o--|| actor_states : "vergleicht" + automation_actions }o--|| actor_commands : "schickt" + automation_actions ||--o{ automation_action_params : "mit" + floors ||--o{ automations : "Reiter" + energiefluss }o--|| floors : "keine Beziehung, nur Abweichungen" +``` + +### Geräte + +| Tabelle | Inhalt | Geschrieben von | +|---|---|---| +| `actors` | ein Gerät je Zeile. `url` entscheidet den Weg: `mqtt://`, `http://`, `wled://`, `io://`/`rts://`/`internal://`/`ogp://` (Tahoma), `Logic`, `Automatik`. `room_id` verknüpft mit `rooms` | Gerätesuchlauf; `room_id` von Hand (Einstellungen → Geräte) | +| `actor_states` | alles Lesbare. `url` ist **entweder** ein MQTT-Topic **oder** ein Feldname im HTTP-JSON, `value_path` der Schlüssel darin, `current_value` der letzte Wert | Suchlauf (Adressen), Runner (Werte) | +| `actor_commands`, `command_parameters` | alles Schaltbare und die Parameter dazu (`url` = Stelle im Befehl) | Suchlauf | +| `state_types` | Datentypen: `integer`, `float`, `bool`, `string`, `time`, `date`, `datetime`, `deltatime`, `elapsed` … | fest | + +Zwei Dinge, die immer wieder überraschen: + +* **`sensors`/`sensor_states` sind unbenutzt.** Der Suchlauf legt auch reine + Messgeräte in `actors`/`actor_states` ab — ein Gerätebegriff für alles. +* **Der Suchlauf löscht nie** (`clear_tables = false`). Er schreibt mit + `ON DUPLICATE KEY UPDATE` auf den URLs. Ein `TRUNCATE` würde neue IDs + vergeben und **alle Automatiken auf falsche Geräte zeigen lassen**. + Karteileichen räumt man in den Einstellungen weg + (→ [einstellungen.md](einstellungen.md#5-geräte)). + +### Automatiken + +Eigenes Dokument: [automatiken.md](automatiken.md#1-datenmodell). Kurz: +`automations` (Rahmen + Laufzustand), `automation_conditions` (Auslöser, +`group_no` = UND/ODER), `automation_actions` + `automation_action_params` +(was geschickt wird), `automation_log` (30 Tage), `calendar_days` (Ferien +und Feiertage, jährlich per `fetch_calendar.py`). + +### Grundriss und Anzeige + +| Tabelle | Inhalt | Besonderheit | +|---|---|---| +| `floors` | Etagen: `code` (fest — steht in URLs, SVG-Ids und `automations.floor`), Bezeichnung, Reihenfolge, Grundrissbild, Standard-Etage | Schema `homeMesh_grundriss.sql` | +| `rooms` | feste Nummer `id`, Etage, Name, `kuerzel` (SVG-Ids), Kachelposition `x`/`y` (leer = keine Kachel), `thermostat` (MQTT-Zweig), `werte` (JSON, leer = Vorgabe) | die Nummer überlebt Umbenennen und Umziehen | +| `energiefluss` | **nur Abweichungen** der Solar-Übersicht vom Katalog: `aktiv`, `name`, `x`/`y`, `optionen` | keine Zeile = Katalogwert; Schema `homeMesh_energiefluss.sql` | + +Die drei `view_*`-Objekte sind Lesehilfen aus der Anfangszeit des +Gerätesuchlaufs; die Anwendung benutzt sie nicht. + +--- + +## 2. `solarLog` — was war + +Stand heute rund 170 MB, verteilt auf wenige große Reihen: + +| Tabelle | Zeilen | Inhalt | Geschrieben von | +|---|---:|---|---| +| `EnergyFlow` | ~108.000 | Momentanleistungen alle 5 min: PV, Netz, Batterie, Etagen, Heizstab, Wallbox | `solarManager.py` | +| `EnergyFlow_hourly` | ~38.000 | Stundenarchiv in kWh — die Langzeitquelle | Rollup-Job | +| `stats_daily` | ~1.500 | Tageswerte für die Jahresstatistik, inklusive vorberechneter Größen | Rollup-Job | +| `Heater` | ~632.000 | Heizung und Heizstab | `solarManager.py` | +| `puffertemp` | ~247.000 | Puffertemperaturen | `solarManager.py` | +| `wasser`, `zisterne` | ~300.000 | Wasserzähler und Zisterne | `gatherWaterData.py` | +| `windrad`, `kiga_windrad` | ~255.000 | Windräder | `solarManager.py` | +| `weatherStation`, `weatherHours`, `weatherDays` | ~220.000 | eigene Wetterstation, Vorhersage | Wetterbrücke, Open-Meteo | +| `daylight` | 663 | Sonnenauf- und -untergang je Tag | Vorhersage-Job | +| `simPower` | ~82.000 | Ertragsprognose | Prognose-Job | +| `byd`, `byd_zellen` | 1.559 / 492 | BYD-Speicher: alle 5 min Ladestand, SOH, Temperaturen; alle 15 min 128 Zellspannungen und 64 Temperaturen | `gatherBYDData.py` | +| `skoda`, `skoda_raw`, `skoda_ladepunkte` | ~1.900 | Fahrzeugzustand, Rohantwort, Ladeverlauf im Minutentakt | `gatherSkodaData.py` | +| `gridCosts`, `gasCosts`, `fuelCosts` | 13 | Preiszeitreihen | Einstellungsseite | +| `car`, `Status`, `actors`, `sensors`, `autoActions*`, `WindradLog` | — | Altlasten aus der Vorgängerfassung | — | + +### Drei Stufen der Verdichtung + +```mermaid +flowchart LR + A["EnergyFlow
alle 5 min, Momentanleistung"] -->|"nachts, Rollup"| B["EnergyFlow_hourly
Stunden in kWh"] + A --> C["stats_daily
Tageswerte + vorberechnete Größen"] + A -. "nach 12 Monaten ausgedünnt" .-> A + B --> H2["Historie: Jahr, Jahrzehnt"] + A --> H1["Historie: Monat
der laufende Tag fehlt im Archiv"] + C --> S["Jahresstatistik
ajax/getStats.php"] +``` + +Wichtig beim Auswerten: + +* **Für einen Monat auf `EnergyFlow` rechnen**, sonst fehlt der laufende Tag + (das Archiv wird nur nachts fortgeschrieben). +* **Für Jahre auf `EnergyFlow_hourly`**, weil die Rohdaten nach zwölf Monaten + ausgedünnt werden — wer dort rechnet, zeigt für alte Jahre zu wenig. +* **Für Kennzahlen auf `stats_daily`.** Manches lässt sich aus Tagessummen + gar nicht rekonstruieren, etwa der Eigenverbrauch oder der Solaranteil der + Wallbox: dafür muss je Messwert eine Bedingung ausgewertet werden. +* **Einspeisung kommt aus `gridPfeed`**, dem Zählerregister, nicht aus dem + Saldo `gridP`. Innerhalb eines Fensters gibt es Bezug und Einspeisung + gleichzeitig; der Saldo löscht kurze Bezüge weg. +* **`pv_fehlbetrag_kwh`** gleicht aus, was fehlt, wenn ein Wechselrichter die + Verbindung zur OpenDTU verliert: Senken sind dann vollständig gemessen, die + Erzeugung nicht — ohne die Korrektur wird der Direktverbrauch negativ. + +Die Preistabellen tragen **einen Stichtag und kein Enddatum**; das Ende holt +sich die Auswertung mit `LEAD()` beim nächsten Eintrag. Weil ein Stichtag ein +Datum ist, kann ein Tag nie zwei Tarife haben — die Verdichtung auf Tage +verliert hier also nichts. + +--- + +## 3. `Logins` — Zugang + +| Tabelle | Inhalt | +|---|---| +| `users` | Passkeys, `authKey`, `lastAuth` | +| `addUser` | Einmal-Links, mit denen ein neues Gerät einen Passkey anlegen darf | + +`checkLogin()` in `helper.php` entscheidet in dieser Reihenfolge: + +1. **Heimnetz** (`isLocal()`) — freier Zugang, ohne Anmeldung. +2. Gültige Sitzung mit `authKey`, höchstens zwei Tage alt. +3. Sonst: Anmeldeseite. + +Daraus folgt die wichtigste Sicherheitsregel dieses Projekts: **Was im +Heimnetz ohne Anmeldung sichtbar ist, darf keinen Schlüssel preisgeben.** +Deshalb gehen API-Schlüssel nie an den Browser zurück +(→ [einstellungen.md](einstellungen.md#6-fahrzeug)), und deshalb schaltet die +Oberfläche Geräte über den Server statt direkt. + +--- + +## 4. Pflege und Sicherung + +| Aufgabe | Wer | Takt | +|---|---|---| +| Rohdaten ausdünnen, Stundenarchiv und Tageswerte fortschreiben | Rollup-Job auf der NAS (`solarlog-rollup.sh`) | nachts | +| `automation_log` aufräumen | Runner selbst | täglich, 30 Tage Aufbewahrung | +| `calendar_days` füllen | `fetch_calendar.py` | jährlich per Cron | +| Datenbanksicherung | `backupDB.sh` | per Cron | +| Schema neu aufsetzen | `homeMesh_DB-layout.sql`, `homeMesh_automations.sql`, `homeMesh_grundriss.sql`, `homeMesh_energiefluss.sql` in dieser Reihenfolge, danach die Nachrüstskripte aus `SolarManager/autoActions/` | einmalig | + +--- + +## 5. Wo fange ich an, wenn ich … + +| Vorhaben | Ort | +|---|---| +| … eine neue Messreihe aufzeichnen | Tabelle in `solarLog` anlegen, Schreiber im SolarManager, Leser als `ajax/*.php` | +| … eine Kennzahl in die Jahresstatistik aufnehmen | Spalte in `stats_daily` (Rollup) **und** Eintrag in `ajax/getStats.php` | +| … wissen, warum eine Zahl in zwei Ansichten abweicht | zuerst prüfen, aus welcher Stufe sie stammt (roh, Stunde, Tag) | +| … ein Gerät endgültig loswerden | Einstellungen → Geräte; per SQL nur, wenn keine Automatik darauf zeigt | +| … eine zweite Installation aufsetzen | Schemata einspielen, dann Einstellungen → Grundriss und → Übersicht; Inhalte stehen in der Datenbank, nicht im Code | diff --git a/doku/einstellungen.md b/doku/einstellungen.md new file mode 100644 index 0000000..66efc34 --- /dev/null +++ b/doku/einstellungen.md @@ -0,0 +1,224 @@ +# Die Einstellungsseite + +Alles, was das Haus beschreibt statt es zu programmieren: Preise, Etagen und +Räume, die Solar-Übersicht, die Werte auf den Kacheln, die Zuordnung der +Geräte und der Zugang zum Fahrzeug. **Was hier eingetragen wird, steht in der +Datenbank — nicht im Quelltext.** Eine zweite Installation braucht deshalb +keine Codeänderung, sondern einen Nachmittag auf dieser Seite. + +| Reiter | Speichert nach | Modell (PHP) | Maske (JS) | +|---|---|---|---| +| Preise | `solarLog.gridCosts`, `gasCosts`, `fuelCosts` | `restricted/costs.php` | `settings.js` | +| Grundriss | `homeMesh.floors`, `rooms` | `restricted/grundriss.php`, `rooms.php` | `grundriss.js` | +| Übersicht | `homeMesh.energiefluss` | `ajax/energiefluss.php` | `energieflussEinstellungen.js` | +| Home-Kacheln | `homeMesh.rooms.werte` (JSON) | `restricted/kacheln.php` | `settings.js` | +| Geräte | `homeMesh.actors.room_id` | `restricted/automations.php` | `settings.js` | +| Fahrzeug | `skoda.conf` (außerhalb des Web-Roots) | `restricted/skodaKeys.php` | `settings.js` | + +Ein Endpunkt für alles: `ajax/settings.php?action=…`. Die Maske prüft nichts, +was das Modell nicht auch prüfte — sie zeigt nur die Meldung, die von dort +kommt. + +```mermaid +flowchart LR + UI["settings.php
Reiter aus reiter.php"] --> JS["settings.js · grundriss.js
energieflussEinstellungen.js"] + JS -->|"GET ?action=…"| EP["ajax/settings.php"] + JS -->|"POST ?action=…"| EP + EP --> M1["costs.php"] --> SL[("solarLog")] + EP --> M2["grundriss.php / rooms.php"] --> HM[("homeMesh")] + EP --> M3["kacheln.php"] --> HM + EP --> M4["automations.php
geraeteListe, geraetLoeschen"] --> HM + EP --> M5["skodaKeys.php"] --> CONF[["skoda.conf
/volume1/homes/wagner/SolarManager"]] +``` + +--- + +## 1. Preise + +Drei Tabellen desselben Zuschnitts: **je Zeile ein Stichtag, kein Enddatum.** +Das Ende holt sich die Auswertung beim nächsten Eintrag (`LEAD()`), damit +eine Preiserhöhung eine Zeile ist und nicht zwei Datumsfelder, die +auseinanderlaufen können. + +| Reihe | Felder | Wofür | +|---|---|---| +| Strom | Arbeitspreis, Einspeisung (ct/kWh), Grundpreis (€/Jahr) | Jahresstatistik | +| Gas | Gaspreis (ct/l), Energieinhalt (kWh/l) | Ersparnis des Heizstabs | +| Benzin | Preis (€/l), Verbrauch Verbrenner (l/100 km), Verbrauch Elektro (kWh/100 km) | „Benzin gespart“ | + +Zwei Entscheidungen, die man beim Erweitern kennen sollte: + +* **Gespeichert wird in Euro, eingegeben in Cent** (`faktor` je Feld). Auf + der Rechnung stehen 31,63 ct/kWh — niemand tippt gern 0,3163. +* **Nicht jedes Feld ist ein Preis.** Energieinhalt und Verbräuche stehen + hier, weil sie am Brennstoff und am Fahrzeug hängen, nicht an der Physik. + Beim Auto bewusst als zwei bekannte Verbräuche statt als „kWh je Liter“ — + diese Zahl steht auf keinem Datenblatt. + +Eine weitere Tabelle desselben Zuschnitts ist ein Eintrag in +`preisReihen()`; Endpunkt, Maske und Speicherlogik bleiben unverändert. + +--- + +## 2. Grundriss + +Der Reiter pflegt `floors` und `rooms` — und damit gleichzeitig das Menü, die +Startseite, die Kacheln, die MQTT-Abos und die Etagen-Auswahl im +Automatik-Editor. + +```mermaid +flowchart TB + E["Etage
code, Bezeichnung, Reihenfolge, Bild, Standard"] --> R["Raum
Name, Kürzel, Thermostat-Zweig"] + R --> K["Kachel
x/y auf der Zeichenfläche 400 × 300"] + R --> G["Geräte
actors.room_id"] + E --> A["Automatiken
automations.floor"] +``` + +Regeln, die das Modell durchsetzt: + +* **Das Kürzel einer Etage ist nach dem Anlegen fest.** Es steht in URLs, + SVG-Ids und `automations.floor`; bis zu vier Großbuchstaben oder Ziffern. +* **Gelöscht wird nur, was leer ist.** Eine Etage mit Räumen oder Automatiken + wird abgelehnt — mit der Liste dessen, was im Weg steht. Stilles + Mitlöschen wäre Datenverlust. +* **Die Raumnummer überlebt alles.** Umbenennen und Etagenwechsel ändern sie + nicht, deshalb behalten Geräte, Regler und Automatiken ihren Raum. +* **Kein `x`/`y` heißt: keine Kachel.** Die Position wird im Plan gezogen, + Maße wie in `home.php` (Fläche 400 × 300, Kachel 24 × 19). +* **Thermostat-Zweig** ist der MQTT-Pfad des Raums. Vorschläge kommen aus den + Topics, die gerade auf `/Temp[degC]` enden. +* **Grundrissbilder**: PNG, JPEG oder WebP, geprüft am Inhalt + (`getimagesize`), nicht an der Endung. Hochgeladene landen unter + `tiles/grundriss` — dem einen Verzeichnis, in das der Webserver schreiben + darf, und bewusst außerhalb von Git: Ein Grundriss ist Inhalt dieses + Hauses, nicht Teil der Anwendung. Der Dateiname trägt einen Teil der + Prüfsumme, damit ein neues Bild eine neue Adresse bekommt und nicht im + Browser-Zwischenspeicher hängen bleibt. + +--- + +## 3. Übersicht (Energiefluss) + +Die Solar-Übersicht hat einen **Katalog im Code** +(`js/solar/energieflussKatalog.js`) — jeder Kreis genau einmal, mit Name, +Symbol, Farbe, Elternkreis, Lage und Ringskala. In der Datenbank steht nur, +**was davon abweicht**. + +```mermaid +flowchart LR + KAT["energieflussKatalog.js
der Normalfall"] --> MIX["setzeEinstellungen()"] + DB[("homeMesh.energiefluss
nur Abweichungen")] --> MIX + MIX --> BILD["gezeichnete Übersicht"] +``` + +Änderbar sind: Kreis an/aus, Name, Lage (im Vorschaubild ziehen) und +Optionen, etwa welche Einträge von `P_PVn` zu welcher PV-Fläche gehören. +„Alles auf Katalog zurücksetzen“ löscht schlicht alle Zeilen. + +Der Vorteil dieser Trennung: Eine neue Installation zeigt sofort etwas +Sinnvolles, ohne dass jemand eine Tabelle füllt — und ein Update des Katalogs +erreicht alle Installationen, ohne die Anpassungen zu überschreiben. + +> Die Hochkant-Lage (`hochX`/`hochY`) steht nur im Katalog. Wer Kreise +> verschiebt, ändert also die Queransicht; +> siehe [README](../README.md) → Energiefluss. + +--- + +## 4. Home-Kacheln + +Welche Werte auf einer Kachel stehen, steht als JSON in `rooms.werte` — +höchstens drei je Kachel. Ohne Eintrag gilt `kachelVorgabe()`: mit +Thermostat Soll, Ist und Feuchte, sonst nichts. + +Eine Wertdefinition hat überall dieselbe Form, damit zwischen Maske, +Datenbank und Anzeige nichts übersetzt werden muss: + +| Feld | Bedeutung | +|---|---| +| `topic` | MQTT-Topic, aus dem der Wert kommt | +| `pfad` | Schlüssel in einer JSON-Nachricht (die Shelly-Zähler schicken mehrere Werte in einer) | +| `wechselrichter`, `feld` | statt `topic`: Summe benannter Wechselrichter unter `solarManager/invertersN` | +| `format` | `leistung` (Quelle meldet Watt) oder `leistung_kw` (Quelle meldet Kilowatt) — skaliert selbst zwischen W und kW | +| `einheit`, `stellen` | sonst: Einheit dahinter, Nachkommastellen | +| `negativ` | Vorzeichen drehen, für verkehrt herum eingebaute Zähler | +| `gross` | die Zahl steht groß auf der Kachel | +| `icon` | Name eines Bootstrap-Icons ohne `bi-` | + +Gelesen wird das im Browser von `homeSVG.kachelWert()`. Warum die +Wechselrichter über ihren **Namen** und nicht über die Nummer gesucht +werden: `inverters0` ist eine Position in einer Liste und sagt nichts über +das Dach — ändert sich die Reihenfolge, stünde sonst stillschweigend das +falsche Dach auf der Kachel. + +--- + +## 5. Geräte + +Die Liste zeigt je Gerät: Beschriftung, Art, Raum, einen Raumvorschlag aus +dem MQTT-Topic, die Zahl der Messwerte und Kommandos, wann zuletzt etwas +gemeldet wurde — **und in welchen Automatiken es steckt**. + +Genau deshalb liegt die Liste im Modell und nicht in der Maske: Wer über das +Löschen entscheidet, muss die Verweise kennen. + +| Handlung | Regel | +|---|---| +| Raum zuordnen | `actors.room_id`; ordnet die Geräteliste im Automatik-Editor und bestimmt, welche Messwerte bei einer Kachel oben stehen | +| Gerät löschen | nur, wenn **keine** Automatik darauf zeigt. Sonst Ablehnung mit den Namen der Automatiken. Gelöscht wird von innen nach außen: Parameter, Kommandos, Messwerte, Gerät — in einer Transaktion | +| Wieder da? | Steht das Gerät noch im Netz, findet es der nächste Suchlauf wieder. Das ist kein Fehler, sondern sein Zweck | + +Der Suchlauf selbst löscht nie (→ [datenbank.md](datenbank.md#geräte)) — +deshalb braucht es diesen Papierkorb überhaupt. + +--- + +## 6. Fahrzeug + +Hier werden die Schlüssel der MyŠkoda-API hinterlegt. Sie stehen in +`skoda.conf` **außerhalb des Web-Roots**, weil sie auch `gatherSkodaData.py` +im SolarManager liest. + +**Ein Schlüssel geht nie an den Browser zurück.** Die Seite steht im +Heimnetz ohne Anmeldung offen, und so ein Schlüssel steuert das Fahrzeug von +überall auf der Welt — Laden, Klimatisierung, Standort. Die Maske sieht nur, +*ob* einer hinterlegt ist und auf welche vier Zeichen er endet. Das genügt, +um zwei auseinanderzuhalten, und taugt zu nichts sonst. Geschrieben wird nur +in eine Richtung. + +Dazu gehört die Aufteilung des Kontingents (20 Anfragen je Stunde für das +ganze Konto, nicht je Schlüssel): + +| Zweck | Anteil | +|---|---| +| Abruf des Fahrzeugzustands (`gatherSkodaData.py`) | 14 / Stunde | +| Befehle aus der Oberfläche (`ajax/skodaCmd.php`) | 4 / Stunde | +| Reserve | 2 / Stunde | + +Ein eigener Befehlsschlüssel trennt deshalb nicht die Kontingente, sondern +nur, wer womit fragt — er lässt sich einzeln widerrufen. + +--- + +## 7. Was die Seite bewusst **nicht** kann + +* **Geräte anlegen.** Das macht der Suchlauf + (`restricted/deviceDiscovery/`); von Hand angelegte Geräte hätten keine + Verbindung zur Wirklichkeit. +* **Automatiken bearbeiten.** Dafür gibt es den Editor auf der Home-Seite + (→ [automatiken.md](automatiken.md#2-der-editor)). +* **Die Standheizung des Fahrzeugs starten.** Das verlangt die + Sicherheits-PIN; sie dauerhaft auf der NAS zu hinterlegen ist eine andere + Größenordnung als ein API-Schlüssel und soll bewusst entschieden werden. + +--- + +## 8. Wo fange ich an, wenn ich … + +| Vorhaben | Ort | +|---|---| +| … eine weitere Preistabelle | Eintrag in `preisReihen()` (`restricted/costs.php`) | +| … ein neues Feld auf einer Kachel | `kachelWertPruefen()`/`kachelKatalog()` in `kacheln.php` und `homeSVG.kachelWert()` in `homeMQTT.js` | +| … eine Etage, einen Raum, ein Grundrissbild | nur diese Seite — kein Code | +| … einen weiteren Reiter | `restricted/settings.php` (Fläche über `reiter.php`), Endpunkt in `ajax/settings.php`, Modell in `restricted/`, Maske in `settings.js` | +| … einen weiteren Zugangsschlüssel verwalten | `restricted/skodaKeys.php` als Vorbild: Modell kennt die Datei, Maske sieht nur den Stand |