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
│   ├── 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

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, 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:

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:

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:

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)

Loginsusers (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.

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:

{ "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

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 Rahmenbedingungen. 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 ausei­nanderlä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 MoFr, 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 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
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:

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

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%