adminandClaude Opus 5 e525731264 Strassenmarkierung und Tempo noch einmal halbiert
Die Striche waren immer noch zu praesent. Jetzt liegt die Strasse im Viertel
des Fahrzeugmassstabs: Leitlinie 157,5 auf 315 Luecke, 3,15 breit, rechts
weiterhin durchgezogen. Das Verhaeltnis 1:2 bleibt, die Markierung ist
Beiwerk statt Hauptsache.

Das Tempo ist mit halbiert - 437 statt 875 Einheiten je Sekunde. Weil auch
die Strichlaenge halbiert ist, ziehen die Striche im selben Takt vorbei wie
zuvor; die Bewegung selbst ist ruhiger. Der Fahrtwind laeuft weiterhin genau
so schnell wie die Fahrbahn (nachgemessen: 437 gegen 436 Einheiten je
Sekunde).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 19:05:02 +02:00

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
  2. Wie eine Seite entsteht
  3. Zwei Wege für Daten
  4. Die Seitenkarte
  5. Die JavaScript-Schicht
  6. Die PHP-Schicht
  7. Die Datenbanken
  8. Vier Bausteine im Detail
  9. Wiederkehrende Bauteile und Konventionen
  10. Wo fange ich an, wenn ich … ändern will
  11. Entwickeln und Prüfen

Auf einen Blick

smart/
├── index.php              Router: Anmeldung prüfen, Seite auswählen, drei Includes
├── helper.php             Session, Heimnetzerkennung, checkLogin(), httpGetJson()
├── authServer.php         Passkeys (WebAuthn): Registrierung und Anmeldung
├── addUser.php            Einmal-Link zum Anlegen eines neuen Schlüssels
├── .htaccess              sperrt .git, *.py, *.sql, *.ini … für den Browser
│
├── restricted/            Vorlagen (was man sieht) und Modelle (was es weiß)
│   ├── header.php footer.php     Rahmen jeder Seite, Menü, Skript-Einbindung
│   ├── home.php solar.php heat.php history.php
│   ├── skoda.php weather.php settings.php logs.php     ← die acht Seitenvorlagen
│   ├── rooms.php                 Räume, Etagen, Kachelwerte, Symbole
│   ├── costs.php                 Preistabellen + solarDb()
│   ├── automations.php           Automatiken, Gerätekatalog + meshDb()
│   ├── kacheln.php               Anzeigewerte der Home-Kacheln (Modell zur Maske)
│   ├── roomControls.php          Bedienelemente des Raum-Modals
│   ├── commands.php              ein Kommando tatsächlich abschicken
│   ├── wallboxen.php             beide Wallboxen über MQTT
│   ├── logdateien.php            Logdateien lesen
│   ├── aussenplan.php            Schrägbild des Außengeländes (SVG, gerechnet)
│   ├── reiter.php                das eine Reiter-Bauteil der ganzen Seite
│   ├── config.php                Zahlen, die an der Wirklichkeit hängen
│   ├── mysql.php                 Zugangsdaten — NICHT im Git (mysql.php.example)
│   └── deviceDiscovery/          Python: Geräte suchen und in homeMesh eintragen
│
├── ajax/                  Endpunkte. Verteiler, keine Logik (Ausnahmen unten)
├── js/solar/              der eigene JavaScript-Code
├── js/                    Fremdbibliotheken, alle lokal (kein CDN)
├── css/solar.css          das eigene Stylesheet (Theme + alle Bauteile)
├── assets/img/            Grundriss-Renderings OG/EG/UG/AG, Icons
├── tiles/                 Kartenkacheln-Zwischenspeicher (nicht im Git)
└── *.sql                  Schema-Dateien zum Nachziehen von Hand

Wie eine Seite entsteht

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:

"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

flowchart LR
  subgraph Browser
    JS["js/solar/*.js"]
    SVG["SVG-Grundriss<br/>Diagramme<br/>Masken"]
  end

  BROKER{{"MQTT-Broker<br/><small>wss://mqtt.nas.el-wa.org:443</small>"}}
  AJAX["ajax/*.php"]
  MODELL["restricted/*.php<br/><small>Modell-Schicht</small>"]
  SOLARLOG[("solarLog<br/><small>Verlauf</small>")]
  HOMEMESH[("homeMesh<br/><small>Geräte, Automatiken</small>")]
  GERAETE["Geräte<br/><small>Tahoma · WLED · Shelly · Wallboxen</small>"]

  BROKER -->|"laufende Werte"| JS
  JS --> SVG
  JS <-->|"fetch: Verlauf, Struktur, Masken"| AJAX
  AJAX --> MODELL
  MODELL --> SOLARLOG
  MODELL --> HOMEMESH
  MODELL -.->|"Kommandos"| BROKER
  MODELL -.->|"HTTP · Tahoma · WLED"| GERAETE

  classDef speicher fill:#eaf5ee,stroke:#6fa981
  classDef code fill:#fff6e5,stroke:#d0a548
  class SOLARLOG,HOMEMESH,BROKER speicher
  class JS,AJAX,MODELL code

Die Regel dahinter, und sie gilt ohne Ausnahme:

Was Weg Warum
Aktueller Messwert (Leistung, Temperatur, Ladestand) MQTT über wss, direkt vom Broker Er weiß es ohnehin, und er sagt es von selbst — kein Nachfragen im Takt
Verlauf, Statistik, Gerätelisten, Formulare fetch auf ajax/*.php Das weiß nur die Datenbank
Jeder Knopfdruck (schalten, speichern, Fahrzeugbefehl) immer über den Server Zugangsdaten dürfen die Seite nicht verlassen, und der Broker ist von außen nicht erreichbar

Der Browser abonniert je nach Seite solarManager/#, weatherStation/#, wattpilot/#, go-eCharger/#, Gartenwasser/# und — auf der Home-Seite — genau die Topics, die auf den Kacheln stehen (tileTopics() in rooms.php).


Die Seitenkarte

Seite (?action=) Vorlage Seitenskripte holt per fetch hört auf MQTT
solar (Vorgabe) solar.php jahresstatistik.js, solarMQTT.js getProdData, getConsData, getForecastData, getSunrise, getStats, carEG, carOG, heater, watering solarManager/#, weatherStation/#, wattpilot/#, go-eCharger/#, Gartenwasser/#
home home.php autoActionFuncs.js, homeMQTT.js room.php, AutoAction.php die Topics der Kacheln, Raumtemp/#
heat heat.php heatMQTT.js getHeaterData, getWaterData, getSunrise solarManager/#, weatherStation/#, Wallbox-Topics
history history.php jahresstatistik.js, historyMQTT.js energyHistory (6 ×), getStats
skoda skoda.php skodaMQTT.js skoda.php?was=…, skodaCmd.php, tile.php solarManager/#
weather weather.php weatherMQTT.js nichts — das Meteogramm ist ein fremdes Dokument im <iframe> (js/meteogram.js) weatherStation/#
settings settings.php settings.js settings.php?action=…
logs logs.php logs.js logs.php?action=…

Und dasselbe als Abhängigkeitsbild — von der Seite bis in den Speicher:

flowchart TB
  subgraph S["Seiten"]
    HOME["home"]; SOLAR["solar"]; HIST["history"]; HEAT["heat"]
    SKODA["skoda"]; SET["settings"]; LOGS["logs"]; WEA["weather"]
  end

  subgraph J["js/solar"]
    COMMON["common.js<br/><small>immer geladen</small>"]
    HOMEJS["homeMQTT.js"]; AUTOJS["autoActionFuncs.js"]; SOLJS["solarMQTT.js"]
    HISTJS["historyMQTT.js"]; STATJS["jahresstatistik.js"]; HEATJS["heatMQTT.js"]
    SKODAJS["skodaMQTT.js"]; SETJS["settings.js"]; LOGJS["logs.js"]; WEAJS["weatherMQTT.js"]
  end

  subgraph A["ajax"]
    ROOM["room.php"]; AUTOA["AutoAction.php"]; SETA["settings.php"]
    STATS["getStats.php"]; EHIST["energyHistory.php"]; PROD["getProdData<br/>getConsData<br/>getForecastData"]
    HEATA["getHeaterData<br/>getWaterData"]; SKODAA["skoda.php<br/>skodaCmd.php<br/>tile.php"]; LOGA["logs.php"]
  end

  subgraph M["restricted (Modell)"]
    ROOMS["rooms.php"]; COSTS["costs.php"]; AUTOM["automations.php"]
    KACH["kacheln.php"]; RCTRL["roomControls.php"]; CMD["commands.php"]; LOGD["logdateien.php"]
  end

  SOLARLOG[("solarLog")]; HOMEMESH[("homeMesh")]; DATEIEN[/"Logdateien"/]; BROKER{{"Broker"}}

  HOME --> HOMEJS & AUTOJS
  SOLAR --> SOLJS & STATJS
  HIST --> HISTJS & STATJS
  HEAT --> HEATJS
  SKODA --> SKODAJS
  SET --> SETJS
  LOGS --> LOGJS
  WEA --> WEAJS
  S -.-> COMMON

  HOMEJS --> ROOM
  AUTOJS --> AUTOA
  SOLJS --> PROD
  HISTJS --> EHIST
  STATJS --> STATS
  HEATJS --> HEATA
  SKODAJS --> SKODAA
  SETJS --> SETA
  LOGJS --> LOGA

  ROOM --> RCTRL & CMD & AUTOM
  AUTOA --> AUTOM
  SETA --> COSTS & KACH & AUTOM
  STATS --> COSTS
  KACH --> ROOMS
  ROOMS --> COSTS
  LOGA --> LOGD

  COSTS --> SOLARLOG
  AUTOM --> HOMEMESH
  CMD --> HOMEMESH
  CMD -.-> BROKER
  PROD --> SOLARLOG
  EHIST --> SOLARLOG
  HEATA --> SOLARLOG
  SKODAA --> SOLARLOG
  LOGD --> DATEIEN

  classDef speicher fill:#eaf5ee,stroke:#6fa981
  class SOLARLOG,HOMEMESH,DATEIEN,BROKER speicher

Die JavaScript-Schicht

Kein Framework, kein Bundler. Jede Datei ist ein Skript, das der Browser so lädt, wie es dasteht; geteilt wird über globale Funktionen aus common.js. Die Namen sind deutsch, sobald es um die Sache geht — englisch nur dort, wo eine Bibliothek es vorgibt.

common.js — immer geladen

Funktion wofür
powerToString(w) Leistung als W oder kW, dieselbe Rundung überall
getData(chart, url, opt) Diagrammdaten holen, eintragen, alle 5 min auffrischen; optional Sonnenauf-/-untergang aus getSunrise.php dazu
submitFormAjax(form) Formular abschicken, ohne die Seite zu verlassen
bindModalSlider() Schieberegler im Modal an ihre Anzeige koppeln
mountMeteogram(), updateWeatherCards() Wetterteile der Solar- und Wetterseite
reiterMerken() offener Reiter steht in der Adresse und übersteht ein Neuladen
loadingHTML(text) der eine Ladezustand für alle Modals

Die Seitenskripte

homeMQTT.js — Grundriss und Kacheln. Zwei Objekte: homeMQTT hält die Verbindung und den Nachrichtenbaum (mqttData), homeSVG schreibt in die SVG-Kacheln. Was auf einer Kachel steht, kommt nicht aus dem Skript, sondern aus homeRooms — der Raumtabelle, die home.php als JSON in die Seite schreibt. homeSVG.kachelWert() ist die Stelle, an der aus einer Nachricht Text wird: Topic oder Wechselrichtersumme lesen, ggf. JSON-Pfad herausgreifen, Vorzeichen drehen (negativ), skalieren (format: "leistung"), runden (stellen), Einheit anhängen. Klick auf eine Kachel öffnet das Raum-Modal über ajax/room.php?room=EG_Bad.

solarMQTT.js — die große Anlagenübersicht. solarSVG zeichnet den Energiefluss (Breite der Pfeile aus der Leistung), darunter hängen die drei Diagramme Prognose, Verbrauch und Erzeugung über getData(). Änderungen am SVG laufen über kleine Setter (setText, setAttr, setFlowWidth) und werden pro Bildrahmen gesammelt (solarSvgUpdatePending), damit ein Schwall MQTT-Nachrichten nicht hundertmal Layout auslöst.

skodaMQTT.js — Fahrzeugseite. Holt alles über ajax/skoda.php?was=… (live, ladungen, kurve, gesundheit, fahrten, strecke), zeichnet das Fahrzeug als SVG-Draufsicht mit Ladestand im Akku, Ladeknopf und Klimaknopf, und die Fahrtenkarte mit Leaflet über den eigenen Kachelspeicher ajax/tile.php. Befehle gehen per POST an ajax/skodaCmd.php — nie direkt an die API.

jahresstatistik.js — ein Bauteil, zwei Seiten. baueJahresstatistik() baut aus der Antwort von getStats.php die Vergleichstabelle (Jahre nebeneinander, Gesamtspalte, Δ in Prozentpunkten bei Quoten, Sparklines je Monat). Die Kennzahlen stehen nicht im Skript: sie beschreiben sich in der Antwort selbst.

autoActionFuncs.js — der Automatik-Editor: Gerätewähler mit Suche, Bedingungen in UND-/ODER-Gruppen, Aktionen mit Parametern, Klartext-Vorschau. Er bekommt Gerätekatalog und Datensatz in einem Dokument von AutoAction.php?action=editor — früher kamen je Bedingung zwei weitere Anfragen dazu, und die Auswahlfelder füllten sich asynchron.

settings.js — drei Reiter in einer Datei, deutlich getrennt: Preistabellen, Home-Kacheln, Geräte. Jeder Abschnitt hat sein Modell (kachelModell, geraeteModell), seine zeichne…()-Funktion und eine …Binden()-Funktion, die genau einen Ereignisbehandler auf den Container legt (Delegation) — deshalb überlebt Bedienung jedes Neuzeichnen.

heatMQTT.js, historyMQTT.js, weatherMQTT.js, logs.js — jeweils klein: Diagramme füllen bzw. Logzeilen nachladen und einfärben.


Die PHP-Schicht

Endpunkte (ajax/)

Endpunkt Aufruf liefert / tut
room.php ?room=EG_Bad, POST ?action=command|temp Raum-Modal bauen; ein Kommando oder eine Solltemperatur senden
AutoAction.php ?action=editor|list, POST save|delete|toggle Automatik-Editor und -Übersicht
settings.php ?action=list|kacheln|geraete, POST save|kacheln-save|geraete-raeume|geraet-loeschen Einstellungsseite
getStats.php GET Jahresstatistik, alle Jahre, mit Metadaten je Kennzahl
energyHistory.php ?series=prod|cons&range=month|year|decade Verbrauchs-/Erzeugungsverlauf (Rohdaten oder Stundenarchiv)
getProdData getConsData getForecastData getHeaterData getWaterData ?FROM=&TO= Chart.js-Datensätze für die jeweilige Karte
getSunrise.php ?FROM=&TO= Sonnenauf- und -untergänge als Diagramm-Markierungen
skoda.php ?was=live|ladungen|kurve|gesundheit|fahrten|strecke Fahrzeugauswertungen aus solarLog.skoda
skodaCmd.php POST befehl=… Laden/Klima/Lüftung am Fahrzeug
tile.php ?z=&x=&y= Kartenkachel aus dem eigenen Zwischenspeicher, sonst einmalig von OSM
logs.php ?action=list|read Zustand und Zeilen der Logdateien
carEG.php carOG.php carSteuerung.php heater.php watering.php tahoma.php Wallboxen, Heizstab, Bewässerung, Tahoma-Bedienung
chartData.php (kein Endpunkt) gemeinsame Bausteine der Chart-Endpunkte
phpMQTT.php (Bibliothek) MQTT aus PHP heraus, für Kommandos

Die meisten Endpunkte sind Verteiler: Eingabe prüfen, Modellfunktion rufen, JSON zurückgeben. Wo Logik in einem Endpunkt steht, hat das einen Grund, der oben in der Datei erklärt ist (getStats.php rechnet die Kennzahlen, energyHistory.php wählt die Quelle nach Zeitraum, skoda.php bildet Fahrten und Ladungen aus einer unregelmäßigen Zeitreihe).

Modelle (restricted/)

Datei Zuständig für Wichtige Funktionen
rooms.php Räume, Etagen, Kachelwerte, Symbole allRooms(), roomsWithTile(), tileTopics(), kachelVorgabe(), kachelUeberschreibungen(), bootstrapIcons(), kachelIcon()
costs.php Preistabellen, Verbindung zu solarLog solarDb(), preiseLaden(), preiseSpeichern(), grundpreisImZeitraum()
automations.php Automatiken, Gerätekatalog, Verbindung zu homeMesh meshDb(), deviceCatalog(), loadAutomation(), saveAutomation(), geraeteListe(), geraetLoeschen(), saveRooms()
kacheln.php Anzeigewerte der Home-Kacheln (Maskenseite) kachelListe(), kachelKatalog(), kachelWertPruefen(), kachelWerteSpeichern(), kachelSymbole()
roomControls.php Welches Gerät welches Bedienelement bekommt bedienform(), zeichneBeschattung(), zeichneLicht(), zeichneMesswerte()
commands.php Ein Kommando abschicken (MQTT/WLED/HTTP/Tahoma) executeCommand(), sendeMqtt(), sendeWled(), sendeHttp(), sendeTahoma()
wallboxen.php Beide Wallboxen über MQTT wallboxen(), wallboxSetzen()
logdateien.php Logdateien vom Ende her lesen logDateien(), logLesen(), logZerlegen()
aussenplan.php Schrägbild des Außengeländes Projektion + SVG
reiter.php Reiter reiterLeiste(), reiterFlaecheAuf/Zu(), reiterBlock()
config.php Zahlen, die an der Wirklichkeit hängen (welches Auto an welcher Box)
mysql.php Zugangsdaten, nicht im Git Vorlage: mysql.php.example

Include-Graph der Modellschicht — flach gehalten, damit man einer Zeile bis zur Datenbank folgen kann, ohne zu springen:

flowchart LR
  HELPER["helper.php"] --> MYSQL["mysql.php<br/><small>Zugangsdaten</small>"]
  ROOMS["rooms.php"] --> COSTS["costs.php"] --> MYSQL
  KACH["kacheln.php"] --> ROOMS
  KACH --> COSTS
  KACH -. "nur wenn vorhanden" .-> AUTOM["automations.php"]
  AUTOM --> MYSQL
  AUTOM --> ROOMS
  CMD["commands.php"] --> MYSQL
  CMD --> TAH["tahoma_EG.php"]
  CMD --> PHPMQTT["ajax/phpMQTT.php"]
  RCTRL["roomControls.php"] --> REITER["reiter.php"]
  HOME["home.php"] --> ROOMS
  HOME --> REITER
  SET["settings.php"] --> COSTS & KACH & REITER
  HEADER["header.php"] --> ROOMS

Die Datenbanken

Drei Schemata, klar getrennte Zuständigkeit:

erDiagram
  actors ||--o{ actor_states : "meldet"
  actors ||--o{ actor_commands : "kann"
  actor_commands ||--o{ command_parameters : "nimmt"
  automations ||--o{ automation_conditions : "wenn"
  automations ||--o{ automation_actions : "dann"
  automation_conditions }o--|| actor_states : "vergleicht"
  automation_actions }o--|| actor_commands : "schickt"
  automation_actions ||--o{ automation_action_params : "mit"

homeMesh — Geräte und Automatiken (Schema: homeMesh_DB-layout.sql, homeMesh_automations.sql).

Tabelle Inhalt geschrieben von
actors ein Gerät je Zeile, url bestimmt den Weg (mqtt://, http://, wled://, io://, Logic) Discovery; room von Hand (Einstellungen → Geräte)
actor_states Messwerte: url = Topic oder Feldname, value_path = Schlüssel darin, current_value Discovery (Adressen), Runner (Werte)
actor_commands, command_parameters was ein Gerät kann und welche Parameter dazugehören Discovery
automations, automation_conditions, automation_actions, automation_action_params das Regelwerk Web-Editor
automation_log was wann ausgelöst hat Runner
calendar_days Feiertage und Ferien fetch_calendar.py (SolarManager)
state_types Datentypen der Messwerte (Zahl, Text, Auswahl …) fest

solarLog — alles, was einen Verlauf hat.

Tabelle Inhalt geschrieben von
EnergyFlow, EnergyFlow_hourly Leistungen im Rohtakt und als Stundenarchiv solarManager.py
stats_daily Tageswerte für die Jahresstatistik solarManager.py
Heater, puffertemp, wasser, zisterne, windrad, kiga_windrad Heizung, Speicher, Wasser, Windrad solarManager.py
skoda, skoda_raw Fahrzeugzustand im Verlauf gatherSkodaData.py
weatherStation, weatherHours, weatherDays, daylight, simPower Wetter, Sonnenzeiten, Ertragsprognose Wetterbrücke, Open-Meteo, Prognose
gridCosts, gasCosts, fuelCosts Preiszeitreihen (Stichtag, kein Enddatum) Einstellungsseite (costs.php)
kachelWerte Überschreibungen der Home-Kacheln, JSON je Raum Einstellungsseite (kacheln.php)

Loginsusers (Passkeys) und addUser (Einmal-Links).


Vier Bausteine im Detail

1. Die Home-Kacheln

Was auf einer Kachel steht, hat drei Stufen: die Vorgabe in rooms.php, die Überschreibung in kachelWerte, und die Anzeige im Browser.

flowchart LR
  ROOMS["rooms.php<br/><small>$rooms: Vorgabe je Raum</small>"] --> MIT["mitWerten()"]
  KW[("kachelWerte<br/><small>je Raum, JSON</small>")] --> MIT
  MIT --> RWT["roomsWithTile()"]
  RWT -->|"als JSON in die Seite"| HOMEJS["homeMQTT.js<br/><small>homeRooms</small>"]
  RWT --> SVG["home.php<br/><small>SVG-Kacheln</small>"]
  TT["tileTopics()"] -->|"abonniert genau diese"| BROKER{{Broker}}
  BROKER --> HOMEJS
  HOMEJS -->|"kachelWert()"| SVG

  SETJS["settings.js<br/><small>Reiter Kacheln</small>"] --> SETA["ajax/settings.php"]
  SETA --> KACHM["kacheln.php<br/><small>prüfen, speichern</small>"] --> KW
  KACHM -->|"Auswahlliste"| KAT["deviceCatalog()<br/><small>automations.php</small>"]

Eine Wertdefinition ist ein kleines JSON-Objekt, überall in derselben Form:

{ "topic": "Power_UG/status/em:0", "pfad": "total_act_power",
  "einheit": "W", "stellen": 0, "negativ": true,
  "icon": "box-arrow-up", "gross": true }

wechselrichter + feld statt topic summiert mehrere Wechselrichter; format: "leistung" skaliert selbst zwischen W und kW; negativ dreht das Vorzeichen (für einen Zähler, der verkehrt herum eingebaut ist); icon ist der Name irgendeines Bootstrap-Icons — die Codepunkte liest bootstrapIcons() aus css/bootstrap-icons.min.css, gezeichnet wird als SVG-Text in der Icon-Schrift.

Fehlt die Tabelle kachelWerte, zeigt jede Kachel ihre Vorgabe: die Startseite ist keine Einstellung, ohne die man das Haus nicht mehr sieht.

2. Das Raum-Modal

sequenceDiagram
  participant B as Browser
  participant R as ajax/room.php
  participant RC as roomControls.php
  participant C as commands.php
  participant G as Gerät

  B->>R: GET ?room=EG_Bad
  R->>RC: Geräte des Raums (actors.room) → Bedienformen
  RC-->>B: fertiges Markup (Rollladen, Licht, Messwerte …)
  B->>R: POST ?action=command {command_id, params}
  R->>C: executeCommand()
  alt mqtt://
    C-->>G: Nutzlast auf das Topic
  else wled://
    C-->>G: JSON an /json/state
  else http://
    C-->>G: Abfrageargumente an die Geräte-URL
  else Tahoma
    C-->>G: exec/apply an die Box
  end

roomControls.php entscheidet an den Fähigkeiten, nicht am Typnamen: Hat das Gerät ein setClosure, wird es ein Rollladen mit Auf/Stop/Zu und zwei Reglern; hat es drei Parameter rot/grün/blau, wird es eine Lampe mit Farbwähler. Ein neu gefundenes Gerät bekommt so ohne Codeänderung ein passendes Bedienelement.

commands.php ist das Gegenstück zu transports.py im SolarManager-Repo — dieselbe Zuordnung „welche URL gehört zu welchem Weg“, einmal für den Runner, einmal für den Browser.

3. Die Automatiken

Editor (autoActionFuncs.js + AutoAction.php + automations.php) schreibt das Regelwerk nach homeMesh; ausgeführt wird es von autoActions/autoaction_runner.py im anderen Repo. Die Weboberfläche schaltet nichts von sich aus — sie beschreibt nur, was gelten soll.

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.

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, Logic); die Datenbanklogik liegt allein im Hauptskript.

Zwei 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.


Wiederkehrende Bauteile und Konventionen

Reiter. Es gibt genau eine Bauart, restricted/reiter.php. Umgeschaltet wird von Bootstrap (data-bs-toggle="tab"), gemerkt wird der offene Reiter von reiterMerken() in common.js. Weniger als zwei Reiter ergeben keine Leiste.

Stylesheet-Reihenfolge — die häufigste Falle. header.php lädt css/solar.css vor css/adminlte.min.css. Eine eigene Regel mit gleicher Spezifität wie eine AdminLTE-Regel verliert deshalb. Wenn eine Änderung „nicht wirkt“, ist das fast immer der Grund: Selektor verlängern (.nav-tabs.reiter .nav-link statt .reiter .nav-link), nicht !important.

Zwischenspeicher. Jedes eigene Skript und solar.css werden mit ?v=<filemtime> eingebunden — gespeichert heißt geladen.

Sprache. Bezeichner und Kommentare sind deutsch, wo es um die Sache geht; englisch bleibt, was von außen vorgegeben ist (floor, topic, Chart.js- Optionen, Spaltennamen). Kommentare erklären warum, nicht was.

Fehlertoleranz vor Vollständigkeit. Was eine Seite nicht zwingend braucht, wird in try/catch geholt: fehlt die Mesh-Datenbank, bleibt die Startseite sichtbar; fehlt kachelWerte, gilt die Vorgabe.

Zugriffsschutz. .htaccess sperrt Punkt-Dateien (allen voran .git) und alle Dateitypen, die der Browser nie anfordert (.py, .sql, .ini, .md …). restricted/mysql.php steht in .gitignore; Vorlage ist mysql.php.example. Zugangsdaten gehören nirgendwo sonst hin.

Zeilenenden. .gitattributes erzwingt LF — der Ordner wird von Linux ausgeliefert.


Wo fange ich an, wenn ich … ändern will

Vorhaben Anlaufstelle
Ein Raum kommt dazu oder heißt anders restricted/rooms.php, Tabelle $rooms — Kachel, Menü und MQTT-Abo ergeben sich daraus
Andere Werte auf einer Kachel Einstellungen → Kacheln (keine Codeänderung). Vorgabe für neue Räume: kachelVorgabe() in rooms.php
Neues Symbol auf einer Kachel Name eines Bootstrap-Icons ins Feld tippen. Vorschlagsliste: kachelSymbole() in kacheln.php
Neue Kennzahl in der Jahresstatistik KENNZAHLEN und kennzahlenAus() in ajax/getStats.php — sonst nichts
Neues Diagramm auf einer Seite Karte in der Vorlage, <canvas>, dazu ein Endpunkt nach dem Muster von getProdData.php, geladen mit getData()
Neue Seite Zeile in $pages (index.php), Vorlage unter restricted/, Skript unter js/solar/
Anderes Bedienelement für ein Gerät bedienform() in roomControls.php (nach Fähigkeit, nicht nach Typname)
Neue Geräteart erkennen neues Modul unter restricted/deviceDiscovery/modules/ + Eintrag in der Liste in device_discovery.py
Neuer Geräteweg zum Schalten commands.php (Browser) und transports.py im SolarManager-Repo (Runner)
Aussehen der ganzen Seite css/solar.css, oberster Abschnitt: die --ton-*-Tokens und die Bootstrap-Zuordnung
Reiter irgendwo reiterLeiste()/reiterBlock() aus restricted/reiter.php benutzen, keine eigene Variante bauen
Neue Einstellung Reiter in $reiter (restricted/settings.php), Abschnitt in settings.js, Aktion in ajax/settings.php, Modellfunktion daneben

Entwickeln und Prüfen

Der Ordner ist die laufende Anwendung — es gibt keinen Build und keine Staging-Stufe. Entsprechend gilt:

# PHP-Syntax (auf der NAS; /usr/bin/php ist 8.1 ohne mysqli)
/usr/local/bin/php82 -l restricted/kacheln.php

# JavaScript-Syntax
/var/packages/Node.js_v20/target/usr/local/bin/node --check js/solar/settings.js

# Modellfunktion einzeln ausprobieren, ohne Browser
/usr/local/bin/php82 -r 'require "restricted/automations.php"; print_r(geraeteListe()[0]);'

# Datenbank ansehen (readDB, nur SELECT)
/usr/local/mariadb10/bin/mysql -h 127.0.0.1 -e 'SELECT * FROM homeMesh.actors LIMIT 5;'

Für Masken lohnt eine Probeseite: eine kleine HTML-Datei, die solar.css und das Skript lädt und window.fetch durch eine Attrappe mit echten Daten ersetzt. Damit lässt sich das Zeichnen prüfen, ohne angemeldet zu sein und ohne etwas zu speichern.

Zwei Repositories, beide auf Gitea (gitea.nas.el-wa.org):

Repo Ort Inhalt
admin/Smart-Dashboard /volume1/web/smart dieses hier
admin/SolarManager /volume1/homes/wagner/SolarManager Sammler, Runner, Wecker

Rekursive Suchen (grep -r) laufen über die SMB-Freigabe in Zeitüberschreitungen — auf der NAS selbst sind sie sofort fertig.

S
Description
No description provided
Readme
17 MiB
Languages
PHP 42.8%
JavaScript 31.5%
Python 12.6%
CSS 11.1%
HTML 1.7%
Other 0.3%