Doku: Datenbanken und Einstellungsseite ergaenzt

datenbank.md: die drei Schemata, wer was schreibt, die drei Stufen der
Verdichtung und welche Tabelle fuer welche Auswertung taugt.
einstellungen.md: was jeder Reiter speichert, welche Regeln das Modell
durchsetzt (feste Etagenkuerzel, Loeschsperren, Schluessel gehen nie zurueck)
und was die Seite bewusst nicht kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 07:57:35 +02:00
co-authored by Claude Opus 5
parent a0a8ad04de
commit 93ec545aaa
4 changed files with 417 additions and 5 deletions
+6 -5
View File
@@ -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)**.
---
+2
View File
@@ -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 |
---
+185
View File
@@ -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<br/><small>alle 5 min, Momentanleistung</small>"] -->|"nachts, Rollup"| B["EnergyFlow_hourly<br/><small>Stunden in kWh</small>"]
A --> C["stats_daily<br/><small>Tageswerte + vorberechnete Größen</small>"]
A -. "nach 12 Monaten ausgedünnt" .-> A
B --> H2["Historie: Jahr, Jahrzehnt"]
A --> H1["Historie: Monat<br/><small>der laufende Tag fehlt im Archiv</small>"]
C --> S["Jahresstatistik<br/><small>ajax/getStats.php</small>"]
```
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 |
+224
View File
@@ -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<br/><small>Reiter aus reiter.php</small>"] --> JS["settings.js · grundriss.js<br/>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<br/><small>geraeteListe, geraetLoeschen</small>"] --> HM
EP --> M5["skodaKeys.php"] --> CONF[["skoda.conf<br/><small>/volume1/homes/wagner/SolarManager</small>"]]
```
---
## 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<br/><small>code, Bezeichnung, Reihenfolge, Bild, Standard</small>"] --> R["Raum<br/><small>Name, Kürzel, Thermostat-Zweig</small>"]
R --> K["Kachel<br/><small>x/y auf der Zeichenfläche 400 × 300</small>"]
R --> G["Geräte<br/><small>actors.room_id</small>"]
E --> A["Automatiken<br/><small>automations.floor</small>"]
```
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<br/><small>der Normalfall</small>"] --> MIX["setzeEinstellungen()"]
DB[("homeMesh.energiefluss<br/><small>nur Abweichungen</small>")] --> 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 |