Commit Graph
32 Commits
Author SHA1 Message Date
adminandClaude Opus 5 00f5944a7a Geraeteauswahl: nach Art gruppiert, Namen geputzt
Nach dem Discovery-Lauf stehen 51 Geraete in der Ausloeser-Liste und 40 in
der Aktions-Liste, flach und alphabetisch. Dreizehn Thermostate und elf
Jalousien sind darin vierundzwanzig gleich aussehende Zeilen.

Die Liste ist jetzt nach Geraeteart gruppiert (optgroup, das koennen auch
die Handy-Browser nativ). Arten mit nur einem Geraet bekommen keine eigene
Ueberschrift - fuenfzehn Gruppen mit je einem Eintrag waeren
unuebersichtlicher als die flache Liste -, sie sammeln sich am Ende unter
"Einzelne".

Dazu ein Namensputz fuer die Anzeige, ohne die Datenbank anzufassen: das
u_-Praefix aus den Tahoma-Beschriftungen faellt weg, ebenso eine
angehaengte einstellige Kanalnummer, und Unterstriche werden zu Leerzeichen.
Aus "u_Wozi Terrassentuer" wird "Wozi Terrassentuer", aus "Power_EG_EM_0"
wird "Power EG EM". Bewusst nur die einstellige Endung - "go-eCharger_270003"
traegt seine Seriennummer und behaelt sie.

Beschriftungen, die danach doppelt waeren, bekommen die Geraeteart dazu: die
Tahoma-Box nennt sowohl den Rollladen als auch den Sonnenenergie-Sensor
"Terasse", in der Liste untereinander waren sie nicht zu unterscheiden.
Daraus wird "Terasse (Rollladen)" und "Terasse (Licht)".

Die Beschriftung entsteht an einer Stelle und wird von Editor und
Uebersicht geteilt - sonst stuende in der Auswahlliste "Wozi Terrassentuer"
und im Satz darunter weiter "u_Wozi Terrassentuer".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 16:02:49 +02:00
adminandClaude Opus 5 9f8c9016c2 Gartenbewaesserung: Morgenfenster, Trockenheits-Notlauf, Regen von heute
Vier Aenderungen an auto_watering.py:

Zeitfenster morgens statt nur abends. Ueber --schedule laesst sich
"morning", "evening" oder "both" waehlen; morgens reicht das Fenster vom
Sonnenaufgang bis SUNRISE_FOLLOW_MINUTES danach, abends wie bisher von
WATERING_LEAD_MINUTES vor Sonnenuntergang bis zum Sonnenuntergang. Im Modus
"both" haelt die 12-Stunden-Sperre aus RECENT_WATERING_LOOKBACK_HOURS das
Ganze selbstregulierend: zweimal am Tag wird nur bei langen Sommertagen
wirklich bewaessert.

Eigener Ausloesezeitpunkt je Fenster. Die Fenster sind mit 70 Minuten
breiter als das stuendliche Cron-Raster, es fallen also je nach Sonnenzeit
zwei Laeufe hinein. Auf das LastWatering-Topic der Ventilsteuerung ist als
Schutz kein Verlass - es wird unter Umstaenden erst spaeter aktualisiert,
und zu kurze Laeufe werden bewusst ignoriert. Das Skript merkt sich seinen
Auslusezeitpunkt deshalb selbst im retained RainStatus der Zone und loest
pro Fenster und Kalendertag nur einmal aus. Nachrichten der Vorversion, die
nur ein "last_trigger" kannten, werden konservativ fuer beide Fenster
uebernommen, damit am Umstellungstag nicht doppelt bewaessert wird.

Der heutige Regen zaehlt mit. Bisher wurden nur abgeschlossene Tage
summiert - hatte es seit dem Morgen geschuettet, half das der Entscheidung
am Abend nicht. Jetzt kommt der bereits gefallene Regen des laufenden Tages
aus den stuendlichen Werten dazu, ohne die Vorhersage fuer die restlichen
Stunden. Das Fenster verschiebt sich entsprechend: threshold_days zaehlt
heute als letzten Tag mit.

Trockenheits-Notlauf. Unabhaengig vom Regen wird bewaessert, wenn die
letzte Bewaesserung laenger als max_dry_days zurueckliegt - fuer die
Hochbeete auf fuenf Tage gesetzt, fuer Troege und Garten vorn aus. Ist kein
Zeitpunkt bekannt, wird nicht ausgeloest: sonst erzwaenge jeder
Broker-Neustart eine Bewaesserung.

Geprueft mit --dry-run in beiden Modi: die Fenster werden richtig gerechnet
(06:36-07:46 und 18:52-20:02), und die Zonen entscheiden wie erwartet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:49:09 +02:00
adminandClaude Opus 5 17678da824 Zahlencodes als Klartext
Manche Geraete schicken eine Zahl und meinen einen Zustand - der
go-eCharger etwa 2 fuer "Charging". Welche Zahl welchen Namen hat, steht im
value_template der Home-Assistant-Discovery als Werttabelle:

    {{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int] }}

Das ist kein Pfad in die Nutzlast, sondern eine Uebersetzung, und sie wurde
bisher verworfen. Jetzt landet sie in possible_values - in derselben
Schreibweise, die WLED fuer seine Effektliste schon benutzt, naemlich einer
Liste aus {Wert: Bezeichnung}. Damit versteht der Editor sie ohne
Zusatzarbeit und macht eine Auswahlliste daraus. Ein Versatz im Ausdruck
wandert in die Schluessel: aus [value_json|int-3] wird {"3":"Default"}.

Der Runner uebersetzt beim Lesen, eine Bedingung vergleicht also den
Klartext. Steht die Zahl nicht in der Tabelle, bleibt sie stehen - ein
erfundener Name waere schlimmer als ein roher Wert.

Dabei ist ein Folgefehler aufgefallen: Auswahlwerte in dieser Schreibweise
kamen im Editor als "[object Object]" an. Die alte addOptions kannte die
Form, der neue Modell-Aufbau nicht - aufgefallen ist es nie, weil WLED beim
Umbau abgeschaltet war. Der Katalog liefert Auswahlwerte jetzt einheitlich
als {value, label}, und zwar seitenrichtig:

    Messwert   angezeigt Charging, gespeichert Charging
               (der Runner hat schon uebersetzt)
    Parameter  angezeigt Blink,    gespeichert 1
               (das Geraet will die Zahl)

Die Uebersicht loest die Bezeichnung ebenfalls auf: "Effekt (Blink)" statt
"Effekt (1)".

Nachgemessen: alle fuenf Werttabellen auf dem Broker richtig erkannt,
einschliesslich des Versatzes, kein Fehltreffer unter den 35 uebrigen
Vorlagen. Live liefert der go-eCharger jetzt Idle, Neutral, Auto, Eco und
None statt 1, 0, 0, 4, 0. Ein weiterer Discovery-Lauf aendert keine Zeile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:44:39 +02:00
adminandClaude Opus 5 56b2ab1648 Mehrere Messwerte auf einem Topic auseinanderhalten
Der go-eCharger schickt sechzehn Zahlen als JSON-Feld auf einem einzigen
Topic, die Wechselrichter und die WLAN-Felder machen es aehnlich. Welcher
Teil der Nutzlast gemeint ist, steht im value_template der
Home-Assistant-Discovery - das Modul las es bisher nur, um den Datentyp zu
raten, und warf es dann weg. Alle sechzehn Messwerte bekamen denselben
Rohtext.

actor_states hat jetzt eine Spalte value_path: ein Pfad in die Nutzlast, in
derselben Schreibweise, die WLED schon benutzt ("[4]", "ssid",
"seg[0].col[0]"). NULL heisst weiterhin: die ganze Nutzlast.

Gelesen wird aus dem Template nur der einfache Fall - ein Zugriff auf
value_json und was danach an Punkten und Klammern folgt. Von den 40
Vorlagen, die hier auf dem Broker liegen, sind 28 genau das. Die uebrigen
zwoelf sind entweder die ganze Nutzlast (dann ist NULL richtig) oder
Werttabellen wie ['Idle','Charging'][value_json|int] - das ist kein Pfad,
sondern eine Uebersetzung von Zahl nach Text, und dort bleibt es beim
bisherigen Verhalten. Kein einziger Fehltreffer: die Werttabellen liefern
sauber None statt eines erfundenen Pfades.

Die Pfad-Auswertung stand schon im WLED-Transport; sie ist jetzt eine
gemeinsame Funktion, die sich beide teilen. MQTT wertet die Nutzlast einmal
je Nachricht aus und verteilt sie danach an alle Messwerte des Topics.

Nachgemessen: die sechzehn nrg-Werte liefern jetzt sechzehn eigene Zahlen
statt sechzehnmal dasselbe, die Leseabdeckung bleibt bei 440 von 441, und
ein weiterer Discovery-Lauf aendert keine Zeile.

Die restlichen geteilten Topics sind kein Fehler: bei den Wechselrichtern
lesen "Limit Persistent" und "Limit NonPersistent" wirklich denselben Wert
und unterscheiden sich nur im Kommando-Topic, und beim Thermostat liest die
Klima-Entity dieselbe Temperatur wie der Sensor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:23:08 +02:00
adminandClaude Opus 5 631baa7333 Discovery: alle Geraete finden und stabil ablegen
Ein Lauf mit allen Modulen brachte 54 Geraete und 441 Messwerte - und fuenf
Fehler ans Licht, die vorher niemand sehen konnte, weil clear_tables die
Tabellen bei jedem Lauf geleert hat.

Vier davon sind Schluessel- und Upsert-Fehler derselben Familie:

 * command_parameters war ueber (command_id, url) eindeutig. WLED-Parameter
   haben keine URL - ihr Name ist der Platzhalter in der Kommando-Vorlage -,
   und eine NULL kollidiert in MySQL nie. ON DUPLICATE KEY UPDATE griff
   also nicht, und jeder Lauf legte dieselben Parameter erneut an: aus 18
   wurden nach drei Laeufen 54. Schluessel jetzt (command_id,
   parameter_name).

 * actor_states war ueber (actor_id, url) eindeutig. Umgekehrtes Problem:
   Messwerte, die sich ein Topic teilen, ueberschrieben einander. Der
   go-eCharger schickt sechzehn Werte als JSON-Feld auf einem Topic - von
   denen kam genau einer in der Datenbank an. Schluessel jetzt (actor_id,
   state_name), das bringt 50 verlorene Messwerte zurueck.

 * Kommandos, Messwerte und Parameter aktualisierten ihre URL beim
   Wiederholungslauf nicht - sie stand nicht im UPDATE-Teil. Eine Korrektur
   in einem Modul kam damit nie in einer bestehenden Datenbank an.

 * Der Typ eines Kommando-Parameters wurde als Text in eine int-Spalte
   geschrieben. MariaDB macht daraus stillschweigend 0, und 0 ist "bool" -
   deshalb bot der Editor fuer die WLED-Helligkeit (0..255) ein Ja/Nein an.

Der fuenfte: die Shelly-Gen1-Relais hatten weder eine Kommando-URL noch eine
URL am Zustand. Ohne die weiss niemand, was zu schicken und wo nachzusehen
ist. Gen1 schaltet ueber ?turn=on|off|toggle und meldet sich in "ison".

Damit entfaellt auch die Uebersetzungstabelle im HTTPTransport: die
Zuordnung gehoert ins Geraetemodell, nicht in den Runner.

Zwei Luecken auf der Runner-Seite, die derselbe Lauf gezeigt hat:

 * WLED liess sich gar nicht ansteuern - es gab keinen Transport dafuer.
   Der neue nutzt die JSON-Vorlage aus command_url, fuellt die Platzhalter
   und schickt sie am Stueck an /json/state.

 * Tahoma wurde am Schema "io://" erkannt. Das Schema beschreibt aber die
   Funkart: dieselbe Box liefert rts:// fuer die Dachfenster und
   internal:// fuer die Alarmanlage. Drei von 22 Geraeten fielen durch.
   Erkannt wird jetzt an der Box-Kennung in der URL.

Nachgemessen: zwei Laeufe hintereinander aendern keine einzige Zeile mehr,
die Geraete-IDs bleiben ueber Laeufe stabil (Voraussetzung fuer die
Fremdschluessel der Automatiken), alle 54 Geraete finden genau einen
Transport, und 440 der 441 Messwerte sind tatsaechlich lesbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 14:57:29 +02:00
adminandClaude Opus 5 0cb1fc635e Etage ist Pflicht
Die Auswahlliste im Editor bot ein "-" an. Eine Automatik ohne Etage taucht
aber in keinem der drei Reiter auf und ist danach nur noch in der Datenbank
zu finden - anlegen und sofort verlieren.

Der Eintrag ist weg, und der Zustand ist jetzt an drei Stellen unmoeglich
statt nur unwahrscheinlich: die Liste kennt nur noch OG, EG und UG,
saveAutomation lehnt alles andere mit einer Meldung ab statt still auf ""
zurueckzufallen, und im Enum der Spalte gibt es den leeren Wert nicht mehr.

Die Reihenfolge in der Liste ist jetzt OG, EG, UG wie bei den Reitern -
damit trifft der Rueckfall auf den ersten Eintrag dieselbe Etage wie die
Startseite. Kennt die Liste die Etage eines Datensatzes nicht, faellt sie
darauf zurueck, statt leer stehen zu bleiben und beim Speichern "" zu
schicken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:32:58 +02:00
adminandClaude Opus 5 27e5c0d331 Sperrzeit je Automatik in drei Stufen
Die steigende Flanke allein schuetzt nicht gegen einen Messwert, der um die
Schwelle pendelt: "Temperatur > 22" bei 22,1 / 21,9 / 22,1 Grad ist jedes
Mal eine echte Flanke, und ueber MQTT koennen die Werte im Sekundentakt
hereinkommen. Gemessen: fuenf Kommandos in einer halben Minute.

automations.lockout_secs sagt jetzt, wie lange nach einer Ausloesung nicht
wieder geschaltet wird. Der Editor bietet drei Stufen an - ohne, eine
Minute, eine Viertelstunde -, weil die passende Wahl am Geraet haengt und
nicht an einer Zahl: ein Rollladen soll nicht alle zwanzig Sekunden
losfahren, eine Lichtfarbe darf das. Gespeichert werden Sekunden, damit eine
vierte Stufe eine Zeile in lockoutChoices() ist und keine Wanderung durch
die Datenbank. Vorbelegt ist eine Minute.

Eine Flanke in der Sperrzeit wird verworfen, nicht aufgehoben. Ein
Rollladen, der eine Viertelstunde spaeter doch noch losfaehrt, weil vor
langer Zeit einmal eine Schwelle gestreift wurde, waere unangenehmer als
einer, der gar nicht faehrt - und der naechste echte Anlass nach Ablauf der
Sperre kommt ohnehin durch. Verworfene Flanken stehen auf DEBUG und nicht in
automation_log, sonst waere die Tabelle bei einem zappelnden Sensor voll
davon.

force_once bleibt unberuehrt: es greift nur, wenn im Fenster gar nichts
gelaufen ist - dann ist auch keine Sperre aktiv.

Nachgemessen am pendelnden Sensor: mit 60 s Sperre ein Kommando, ohne Sperre
fuenf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:26:18 +02:00
adminandClaude Opus 5 25d3c47bd3 Sonnenstand: ueber die Tagesgrenze rechnen statt abschneiden
Ein Versatz, der ueber Mitternacht hinausreicht, wurde bisher am Tagesrand
abgeschnitten. Damit liessen sich "sechs Stunden vor Sonnenaufgang" oder
"fuenf Stunden nach Sonnenuntergang" gar nicht mehr formulieren - solche
Angaben sind aber gewollt und landen eben am Vorabend bzw. nach Mitternacht.

Gerechnet wird jetzt modulo Tag. Verglichen wird die Uhrzeit innerhalb des
Tages: ein Ziel jenseits von Mitternacht gilt als diese Uhrzeit am selben
Tag. Bei "+" und "-" ist das genau der gemeinte Zeitpunkt, bei "ab" und
"vor" verschiebt sich der wahre Bereich entsprechend mit - das steht so in
der README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:10:20 +02:00
adminandClaude Opus 5 ee417f302f Zeit-Ausloeser: "um" ergaenzt, Sonnenstand vervollstaendigt
Bei Uhrzeit und Datum gab es nur "ab" und "vor". Der haeufigste Fall - eine
Aktion genau um 16:30 - liess sich damit gar nicht ausdruecken. "um" ist
jetzt der erste Eintrag und damit die Vorbelegung.

Der Hinweis von frueher bleibt trotzdem richtig und steht in der README:
"um" trifft nur eine einzige Minute, faellt der Runner ausgerechnet in
dieser Minute aus, ist die Automatik fuer den Tag verloren. "ab" holt der
naechste Takt nach. Wer beides will, hakt force_once an.

Beim Sonnenauf- und -untergang fehlte die halbe Tabelle: der Versatz kann
davor oder danach liegen, und verglichen werden kann davor, danach oder
genau. Statt zwei gibt es jetzt sechs Operatoren - "+", "-", "ab +",
"ab -", "vor +", "vor -". Das Vorzeichen steckt im Operator, weil der
Editor eine einzige Auswahlliste zeigt und nicht zwei Bedienelemente fuer
eine Angabe.

Rutscht ein Versatz rechnerisch ueber den Tagesrand (Sonnenaufgang 05:34
minus sechs Stunden), bleibt es beim Tagesrand statt auf die andere Seite
von Mitternacht zu springen - "kurz vor Sonnenaufgang" soll nicht ploetzlich
gestern abend bedeuten.

Der Editor brauchte keine Aenderung: er liest Wert und Beschriftung der
Operatoren aus dem Geraetekatalog, statt sie selbst zu kennen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:30:31 +02:00
adminandClaude Opus 5 5f643b9844 AutoAction-Runner: Automatiken ausfuehren
Bisher konnte man Automatiken nur anlegen - ausgefuehrt hat sie niemand.
restricted/autoActions/autoaction_runner.py holt das nach.

Dauerlaeufer statt Cronjob, aus zwei Gruenden: Schwellwert-Ausloeser sollen
greifen, wenn die MQTT-Nachricht hereinkommt, und actor_states.current_value
wird sonst von niemandem fortgeschrieben - beim Discovery einmal gesetzt und
danach nie wieder. Ein zustandsloser Lauf haette gar nichts, womit er
vergleichen koennte. Der Runner pflegt den Wert nebenbei mit, wovon auch der
Editor profitiert: er zeigt neben jedem Messwert den aktuellen Stand.

Ausgeloest wird nur auf der steigenden Flanke (automations.cond_met), sonst
wuerde "Temperatur ueber 22 Grad" bei jedem Takt erneut feuern. Aus
demselben Grund heissen Zeit-Ausloeser jetzt "ab 16:30" statt "gleich
16:30": ein Gleichheitsvergleich waere nur in einer einzigen Minute wahr,
und ein Ausfall in genau dieser Minute kostet den ganzen Tag. Dieselbe
Ueberlegung steht hinter den breiten Zeitfenstern in auto_watering.py.

Der Weg zum Geraet haengt an der URL des Aktors: mqtt:// abonniert und
publiziert, http:// pollt und haengt Parameter an, io:// spricht mit der
Tahoma-Box, Logic rechnet Uhrzeit, Datum und Sonnenzeiten. Alle vier stehen
in transports.py; eine fuenfte Geraeteart ist eine weitere Klasse mit
passt(), zustaende_lesen() und senden().

fetch_calendar.py fuellt calendar_days aus openholidaysapi.org, damit "in
den Ferien" und "an Feiertagen" eine Grundlage haben. Einmal jaehrlich per
Cron.

Die Uebersicht blendet auf schmalen Schirmen Spalten aus, statt sie
wegzuschieben - auf dem Handy waren die Knoepfe zum Pausieren und Loeschen
sonst nur per Seitwaertsscrollen erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 19:21:07 +02:00
adminandClaude Opus 5 eee3a89b4e AutoAction-Editor: Speichern, Laden und Loeschen
Der Editor las seine Geraete aus homeMesh, schrieb aber nach solarLog in
Tabellen, deren Fremdschluessel auf solarLog.actors/sensors zeigten. Jedes
INSERT lief damit gegen den Fremdschluessel - autoactionsSensors und
autoactionsActors sind deshalb leer geblieben, und die Aktor-Schleife war
ohnehin nie geschrieben worden.

Die Automatiken liegen jetzt in homeMesh neben dem Geraetemodell
(homeMesh_automations.sql). Eine Bedingung verweist mit einem einzigen
Fremdschluessel auf actor_states statt auf Geraet, State-URL und ein
unklares valID; Kommandoparameter stehen zeilenweise statt in vier festen
Spalten "Wert 1" bis "Wert 4". Verknuepft wird ueber group_no: gleiche
Nummer UND, verschiedene ODER, ausgewertet als any(all(gruppe)).

Im Editor ist eine Gruppe ein gerahmter Block mit eigenem "+ Bedingung",
dazwischen steht ODER - die Klammerung ist damit gezeichnet und nicht
vereinbart, und darunter steht derselbe Satz noch einmal in Worten. Die
Uebersicht zeigt ihn in der Spalte "Ausloeser" und ersetzt die bisher fest
verdrahtete Beispielzeile.

Nebenbei behoben: die Operator-Knoepfe schickten ihr innerHTML ("≠",
"&gt;") an eine Whitelist, die "!=" erwartete - jede Bedingung "ungleich"
wurde still zu "gleich". Ausgeblendete Wertfelder sendeten ihren Inhalt
trotzdem mit. Beides entfaellt, weil der Editor jetzt JSON aus einem
Modell schickt statt durchnummerierter Formularfelder.

Die vier Endpunkte fillSensorDD, fillActorDD, sensorDetails und
actorDetails entfallen: der Geraetekatalog reist einmal mit dem Editor
mit, statt je Bedingungszeile zwei Anfragen nachzuladen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 19:07:29 +02:00
adminandClaude Opus 5 fd27fe168e Handy-Darstellung: Diagramme, Meteogramm und Abstaende
Auf dem Handy war die Seite bisher nur mit der Desktopversion des Browsers
brauchbar. Drei Ursachen, alle nachgemessen bei 375 px:

Die Diagramme standen im Hochformat, 301 x 400 px - die Zeitachse lag auf
der kurzen Seite. Die Hoehe kam nicht aus dem Container: .chart-container
hatte inline height:100% in einem Elternelement ohne Hoehe, und die Canvas
trugen height="400px;", was kein gueltiger Zahlenwert ist. Die Hoehe steht
jetzt im Stylesheet, 400 px am Rechner und 250 px unter 768 px Breite, die
Beschriftung dort 8 statt 10 px.

Das Meteogramm hat sich nicht angepasst, sondern gequetscht. meteoblue legt
sein Bild einmal beim Laden fest und zeichnet es bei einer Breitenaenderung
nicht neu; unter etwa 600 px ueberlagern sich Wochentage und Stundenachse.
Es wird jetzt in mindestens 640 px Breite geladen - auf breiten Karten
gleich in Kartenbreite, dort aendert sich nichts - und, wenn die Karte
schmaler ist, als Ganzes verkleinert. Zuschnitt und Skalierung setzt das
Skript direkt am Element, nicht ueber das Stylesheet: sonst haengt die
Darstellung an einer womoeglich veralteten solar.css im Browser-Cache, und
der breit geladene Rahmen ragt ungeschnitten aus der Karte.

Passend dazu bekommt solar.css eine Versionskennung aus filemtime(), wie sie
die Skripte in footer.php und adminlte.min.css laengst tragen. Ohne sie kam
auf schon einmal besuchten Geraeten die alte Datei aus dem Cache.

Die Statistik-Kacheln lagen in nackten .col ohne Breitenangabe; Bootstrap
laesst sie dann nach Textlaenge umbrechen, mal zwei, mal drei je Zeile und
unterschiedlich breit. Die Breite kommt jetzt aus row-cols-2/row-cols-md-4,
und die Karte fuellt die Zeilenhoehe, damit die Werte trotz ein- oder
zweizeiliger Titel auf einer Linie stehen.

Zuletzt sind die Abstaende unter 768 px enger gefasst - auf .app-content
beschraenkt, damit Seitenleiste, Kopfzeile und Modals unberuehrt bleiben:
Spaltenabstand 24 -> 8 px, Kartenabstand 24 -> 12 px, Polsterung 16 -> 8 px.
Die nutzbare Breite steigt damit von 301 auf 317 px.

Geprueft bei 375 und 1200 px auf Solar, Historie, Heizung, Home und Wetter:
kein Querlauf, am Rechner unveraenderte Werte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 23:05:33 +02:00
adminandClaude Opus 5 6aac3326a9 Bewaesserungs-Modal: Tagessummen aus WateringToday und LastWateringDay
Die Ventilsteuerung veroeffentlicht neuerdings je Steuerung zwei weitere
Topics: WateringToday mit der heute schon gelaufenen Zeit je Ventil und
LastWateringDay mit derselben Summe fuer den letzten Tag, an dem ueberhaupt
bewaessert wurde. Beide tauchen als zweite Zeile in der Zonenkarte auf:
"Heute 35m - Vortag 30m".

WateringToday wird gegen das heutige Datum geprueft. Die Nachricht ist
retained; nach einem Tageswechsel ohne Bewaesserung stuende sonst die Summe
von gestern als heutige da. Passt der Tag nicht, zeigt die Karte "--" und
nennt den Stand im Titel. Die Spalte heisst "Vortag" und nicht "Gestern",
weil LastWateringDay einen eigenen wateringDay mitfuehrt - es ist der letzte
Tag mit Bewaesserung, nicht zwangslaeufig der gestrige; das Datum steht im
Titel.

Geprueft: leeres Topic, Nutzlast mit 0 Sekunden, gefuellte Werte, veraltete
Nutzlast, fehlendes Feld, kaputtes JSON, Datumsformat - zehn Faelle, keine
Abweichung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 22:48:25 +02:00
adminandClaude Opus 5 eca5c8f041 Bewaesserungs-Modal: Zonenkarten statt Tabelle
Das Modal ist 500 px breit (modal-dialog ohne Groessenklasse). Die Tabelle
mit fuenf Spalten samt Zahlenfeld war darin schon eng; jede weitere Angabe
haette sie gesprengt. Statt der Tabelle bekommt jede Zone eine eigene Karte
mit Zustandsschild, Steuerzeile und den Zeiten darunter, die Automatik eine
eigene Karte darueber - dort sitzt auch die kombinierte Automatik, die zwei
Zonen bedient.

Die Automatik-Karte zeigt zusaetzlich, was auto_watering.py je Zone vorhat,
und begruendet es: "heute geplant - nur 0,2 mm Regen in 2 Tagen". Die Zahlen
stammen aus dem RainStatus-Topic, das die Seite ohnehin schon abonniert hat,
bisher aber nur fuer das Uhr-Symbol im SVG benutzte. Die genaue Regel steht
im Titel der Zeile, weil sich die Schwellen je Zone deutlich unterscheiden.
Die letzte Bewaesserung bleibt in der Zonenkarte, sie ist eine Eigenschaft
der Zone; die Automatik nennt sie nur als Begruendung.

Nebenbei sind die drei getrennten Zuordnungstabellen (wateringZones,
wateringIndicators, wateringHistoryRows) zu einer verschmolzen, die Ventil,
Automatik-Zone, Beschriftung und die SVG-Anzeige-Ids traegt. Damit wird aus
updateWateringStatus eine Schleife statt dreier, und die doppelt vorhandene
Knopflogik faellt weg. Alle 18 Zuordnungen und die Historienzeilen sind
gegen den vorherigen Stand geprueft, ohne Abweichung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 22:47:50 +02:00
adminandClaude Opus 5 37270acb23 .htaccess: Git-Verzeichnis auch ohne mod_rewrite und mod_alias sperren
Die Regel gegen Punkt-Pfade stand ausschliesslich in IfModule-Bloecken fuer
mod_rewrite und mod_alias. Ist keines der beiden geladen, entfaellt sie
ersatzlos und ohne Fehlermeldung, und .git waere wieder abrufbar - genau das
Fail-Open, das derselbe Commit-Satz bei restricted/.htaccess beseitigen
sollte.

Ergaenzt um eine Notbremse aus Kern-Direktiven, die immer verfuegbar sind:
FilesMatch auf die Einstiegsdateien eines Git-Verzeichnisses (HEAD, config,
index, packed-refs und weitere) sowie pack und idx in der Typenliste. Ohne
diese Dateien laesst sich das Verzeichnis nicht aufrollen, weil die
Objektdateien hexadezimale Namen tragen.

FilesMatch passt nur auf Dateinamen, nicht auf Pfadbestandteile, und kann
.git deshalb nicht vollstaendig sperren. Der Kommentar in der Datei sagt das
und benennt die belastbare Loesung: .git und restricted/ ausserhalb des
Dokumentenbaums legen.

Gegengeprueft: von den 40 Adressen, die die Seiten laden, faellt keine unter
die neuen Regeln; in .git greifen sie zusaetzlich fuer 13 Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:16:26 +02:00
adminandClaude Opus 5 f17d010d5f fillElementArray(): auch den Wert-Cache verwerfen
Die Funktion soll laut eigener Beschreibung den Zwischenspeicher verwerfen,
falls das SVG neu gezeichnet wird, leerte aber nur svgElements. letzterWert
blieb gefuellt, sodass setText, setAttr und setDisplay jeden Schreibzugriff
danach als "unveraendert" ueberspringen: die Knoten werden zwar neu
nachgeschlagen, aber nie beschrieben. Das frische SVG behaelt dann seine
Platzhalter, bis sich der jeweilige Messwert zufaellig aendert - bei den
Boegen mit auf volle Grad gerundeten Winkeln kann das dauern.

Nachgestellt: nach dem Verwerfen nur der Knoten bleibt die Anzeige auf "--",
mit geleertem letzterWert wird wieder geschrieben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:15:24 +02:00
adminandClaude Opus 5 405162ed47 setFlowWidth(): negative Breite bedeutet kein Fluss, nicht Fluss rueckwaerts
Die Funktion bildete die uebergebene Breite mit Math.abs() ab. Die Aufrufer
rechnen 55*(1-exp(x)); kehrt sich das Vorzeichen der Messgroesse um, wird der
Ausdruck negativ. Frueher setzte das eine negative stroke-width und die Linie
blieb leer. Mit Math.abs() erschien stattdessen eine Animation voller Staerke
in unveraenderter Laufrichtung - das Diagramm zeigte einen Fluss, den es nicht
gab.

Beispiel: speist die EG-Etage mit 900 W zurueck, ergibt 55*(1-exp(900/2000))
den Wert -31,3. Math.abs machte daraus eine 31,3 breite laufende Linie, jetzt
sind es 0 und die Linie ruht.

Positive Breiten und die Schwelle von 0,5 bleiben unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:14:54 +02:00
adminandClaude Opus 5 dc85f33e9f ipInNetwork(): Praefixlaenge pruefen statt ungeprueft uebernehmen
Die Laenge wurde mit intval() aus dem CIDR-String uebernommen. Bei einem
Tippfehler in LOCAL_NETWORKS lief die Funktion damit fail-open: "/-5" ergibt
intdiv(-5,8)=0 ganze Bytes und chr((0xFF << 13) & 0xFF)=Nullbyte, womit beide
maskierten Bytes gleich sind und jede Adresse in jedem Netz liegt. isLocal()
haette dann jede Anfrage aus dem Internet als lokal gewertet - und damit als
angemeldet, inklusive Freigabe der Passkey-Registrierung.

Die Laenge muss jetzt aus reinen Ziffern bestehen und darf die Adressbreite
nicht ueberschreiten; sonst liefert die Funktion false. "/-5", "/244", "/abc"
und "/" fallen damit durch, gueltige Angaben bleiben unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:14:25 +02:00
adminandClaude Opus 5 16e236c7f8 Verzeichnisschutz haerten: Web-Root absichern, .htaccess versionsfest machen
restricted/.htaccess enthielt nur "deny from all". Das ist Apache-2.2-Syntax;
unter 2.4 funktioniert sie ausschliesslich mit geladenem mod_access_compat und
quittiert sonst mit einem Fehler. Der Schutz haengt damit an einem Modul, das
es nur aus Abwaertskompatibilitaet gibt. Jetzt stehen "Require all denied"
(2.4) und "Order allow,deny" (2.2) nebeneinander in IfModule-Bloecken.

Neu dazu ein .htaccess im Web-Root, denn restricted/ war nur ein Verzeichnis
von mehreren:

- Punkt-Pfade werden gesperrt. Das Git-Verzeichnis liegt im Web-Root; ueber
  .git liesse sich sonst der komplette Quellcode samt Historie herunterladen.
  Betrifft 65 Dateien, darunter .git/config und .claude/settings.local.json.
  .well-known bleibt frei fuer Zertifikatsausstellungen.
- Dateitypen gesperrt, die der Browser nie anfordert: ini, py, pyc, json, md,
  log, bak, example, sql, sqlite, db, tgz, gz, zip, yml, yaml. Betrifft 24
  Dateien, darunter deviceDiscovery/config.ini mit Zugangsdaten,
  tahoma_devices*.json und die Python-Skripte.

Gegengeprueft: von den 37 Adressen, die die Seiten laden, faellt keine unter
die neuen Regeln. Das Frontend holt ausschliesslich php, css, js, Bilder und
Schriften; tahoma_devices_classic.json liest PHP serverseitig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:53:30 +02:00
adminandClaude Opus 5 48017e895a isLocal() haerten: echte Adressvergleiche statt Zeichenketten
Die Pruefung zerlegte beide Adressen mit explode(":") und verglich die ersten
vier Felder. Bei IPv4 entsteht dabei nur ein Feld, die uebrigen drei sind
undefiniert und null == null ist wahr - die Bedingung schrumpfte damit auf
REMOTE_ADDR == SERVER_ADDR. Kaeme je ein Reverse Proxy auf denselben Host,
haette das jeden Zugriff aus dem Internet als lokal gelten lassen.

Neu:
- LOCAL_NETWORKS als sichtbare Liste, derzeit 192.168.179.0/24.
- ipInNetwork() vergleicht binaer ueber inet_pton, also unabhaengig von der
  Schreibweise. "2001:0db8:a:b::9" gilt jetzt zurecht als dasselbe /64 wie
  "2001:db8:a:b::9" - vorher fiel dieser Client durch.
- Das IPv6-/64 wird weiterhin aus SERVER_ADDR abgeleitet, da der Provider das
  Praefix vergibt.
- REMOTE_ADDR wird mit filter_var geprueft; "192.168.179.44 evil" galt vorher
  wegen str_starts_with als lokal.
- Auf IPv6 abgebildete IPv4-Adressen (::ffff:192.168.179.44) werden
  normalisiert. Ein Dual-Stack-Socket meldet LAN-Clients so; bisher fielen
  sie durch.
- $_SESSION["local"] wird in beiden Zweigen gesetzt, nicht nur im positiven.

Verhaltensaenderung: Anfragen, deren Absender gleich SERVER_ADDR ist - etwa
127.0.0.1 - gelten nicht mehr automatisch als lokal. Im Repo ruft nichts die
Seite ueber HTTP vom Server selbst auf. Wird das doch gebraucht, gehoert das
betreffende Netz in LOCAL_NETWORKS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:43:43 +02:00
adminandClaude Opus 5 768520cedd Geraete-Endpunkte: Anmeldung voraussetzen
actorDetails.php, sensorDetails.php, fillActorDD.php und fillSensorDD.php
fragten die homeMesh-Datenbank ab und gaben Geraetenamen samt Steuer-URLs
(command_url, command_parameters.url) aus - ohne jede Zugriffspruefung.

Alle vier pruefen jetzt checkLogin(), bevor sie etwas ausgeben. Aufgerufen
werden sie ausschliesslich aus js/solar/autoActionFuncs.js auf der Home-Seite,
also aus einer angemeldeten Sitzung; der Regeleditor funktioniert unveraendert.

Die SQL war bereits sauber (intval), es fehlte nur die Autorisierung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:37:49 +02:00
adminandClaude Opus 5 0075f803d0 tahoma.php: Anmeldung voraussetzen
Die Datei hatte keinerlei Zugriffspruefung. Damit liessen sich von beliebiger
Stelle die Somfy-Geraete auflisten (?action=devlist) sowie Rollladen abfragen
und verfahren (?action=move&device=...&pos=...&angle=...). Das Tahoma-Token
liegt serverseitig, ein Angreifer brauchte es also nicht.

checkLogin() ist jetzt vorgeschaltet, dafuer wird helper.php eingebunden. Im
Frontend gibt es derzeit keinen Aufrufer der Datei, ein bestehender Ablauf
kann also nicht brechen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:37:48 +02:00
adminandClaude Opus 5 464c08f414 carEG.php: Befehlseinschleusung ueber das Zeitfeld schliessen
$_POST["ftt"] wurde nur auf "nicht leer" geprueft und dann roh in die
exec()-Kommandozeile gehaengt. Ein Wert wie "12:00 2>&1; befehl #" fuehrte
damit beliebige Shell-Befehle als Web-Benutzer aus.

Das Formularfeld ist ein <input type="time">, liefert also HH:MM. Der Wert
wird jetzt gegen genau dieses Format geprueft; alles andere gilt als "keine
Ladeplanung" und nimmt den vorhandenen else-Zweig. Zusaetzlich gehen ftt und
setTime durch escapeshellarg().

fte und evAmp waren durch die Vergleichs-Klemmung bereits faktisch numerisch,
laufen jetzt aber ueber intval() und eigene Variablen statt ueber $_POST.
Fuer gueltige Eingaben ist die erzeugte Kommandozeile unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:35:59 +02:00
adminandClaude Opus 5 49ff6b5c0b authServer.php: Passkey-Registrierung gegen fremde Aufrufe absichern
getCreateArgs und processCreate waren ohne jede Pruefung erreichbar. Der
Einmalschluessel aus addUser.php steuerte nur, ob index.php die
Registrierungsseite anzeigt - authServer.php liess sich direkt aufrufen und
umging das. Da userId und userName fest hinterlegt sind, konnte damit jeder
mit zwei Requests einen eigenen Passkey fuer das vorhandene Konto anlegen.

checkAdduser() vermerkt eine erfolgreiche Schluesselpruefung jetzt als
$_SESSION["mayRegister"]. authServer.php laesst getCreateArgs, processCreate
und clearRegistrations nur noch durch, wenn dieser Vermerk hoechstens zehn
Minuten alt ist oder der Aufruf aus dem lokalen Netz kommt.

Anmeldung und Metadatenabfrage (getGetArgs, processGet,
queryFidoMetaDataService) bleiben unveraendert offen.

authServer.php bindet dafuer helper.php ein statt nur restricted/mysql.php;
das eigene session_start() entfaellt, helper.php erledigt es.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:34:11 +02:00
adminandClaude Opus 5 64445e95fe Zugangsdaten nur noch aus restricted/mysql.php, Datei nicht mehr versioniert
authServer.php und ajax/getForecastData.php hatten Server, Benutzer, Passwort
und Datenbank noch einmal fest eingetragen und damit die Werte ueberschrieben,
die restricted/mysql.php ohnehin liefert. Beide Dateien binden die Konfiguration
bereits ein; die Doppelung ist entfallen.

getForecastData.php benutzt jetzt $mysql_solarUser/-Pass/-DB wie die uebrigen
Endpunkte, statt dieselben Werte unter den generischen Namen zu setzen.

restricted/mysql.php steht jetzt in .gitignore, restricted/mysql.php.example
dient als Vorlage fuer neue Arbeitskopien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:50:45 +02:00
adminandClaude Opus 5 6b8556567b Dashboard: Doppelungen zusammengelegt, Lade- und Renderlast gesenkt
Zusammenlegung
- Die sechs Endpunkte getConsData_/getProdData_{month,year,decade}.php sind
  ajax/energyHistory.php?series=&range= gewichen, der gemeinsame Aufbau der
  Chart-Daten steht in ajax/chartData.php. Die erzeugte SQL wurde gegen alle
  sechs Originale abgeglichen.
- Die 13 Raeume der Home-Ansicht stehen nur noch in restricted/rooms.php.
  home.php rendert die SVG-Kacheln daraus, homeMQTT.js bekommt dieselbe
  Tabelle als JSON (346 -> 222 bzw. 428 -> 197 Zeilen).
- Gemeinsame JS-Helfer in js/solar/common.js; geteilte Bausteine fuer
  Wetterkacheln, Chart-Skripte und das Formular der Ladesteuerung.
- Seitenvorlagen sind jetzt .php und werden eingebunden, statt per
  file_get_contents zusammengesetzt zu werden.

Fehlerbehebungen
- ?action=weather fuehrte auf eine leere Seite, es gibt jetzt weather.php.
- homeMQTT.js griff auf ein #meteogram zu, das home.php nie hatte, und brach
  damit den readystatechange-Handler ab.
- Die Heizungsseite abonnierte weatherStation/#, zeigte die Werte aber nie an.

Ladelast
- Chart.js, MQTT und Meteogramm werden nur noch auf den Seiten geladen, die
  sie brauchen; die Seitentabelle dafuer steht in index.php.
- mqtt.js durch die minifizierte Fassung ersetzt (859 -> 359 KB). MQTT.js
  5.14.1, byte-identisch mit dem signierten npm-Tarball.
- Rund 3,1 MB ungenutzte Vendor-Reste und AdminLTE-Demobilder entfernt.
- Solar 1796 -> 1240 KB, Home 1796 -> 873 KB, Historie 1796 -> 843 KB.

Speicher im Browser
- Chart.defaults.devicePixelRatio auf 1.5 gedeckelt. Die Zeichenpuffer der
  Diagramme waren mit 20-45 MB der groesste Posten der Seite.
- Das Meteogramm-iframe laedt erst beim Hinscrollen.

Realtime-SVG
- Filterbereiche auf das noetige Mass verkleinert und den wirkungslosen
  feOffset-Durchgang entfernt: 60 % weniger gerasterte Filterflaeche bei
  pixelgleichem Ergebnis.
- Die Fluss-Animation laeuft ueber transform statt stroke-dashoffset, mit
  Punkten statt Strichmuster; sie ruht, wo kein Fluss anliegt.
- Der Wertupdate schreibt nur noch bei echter Aenderung und benutzt
  textContent statt innerHTML: 9 statt 86 Elemente pro Update, davon keines
  mit Filter. describeArc rundet auf volle Grad.
- waterInfo auf scale(0.90) wie die uebrigen Info-Gruppen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:54:28 +02:00
adminandClaude Sonnet 5 29f752eafe Regenbasierte automatische Gartenbewässerung + homeMesh-Anbindung für AutoAction-Regeleditor
- Neues stündliches Python-Script (restricted/gartenbewaesserung/auto_watering.py): holt Niederschlags-
  und Sonnenuntergangsdaten von Open-Meteo, wertet je Zone (Hochbeete/Tröge/Garten vorn) individuelle
  Regen-Schwellenwerte und Mindest-Bewässerungsdauern aus und startet bei Bedarf per MQTT den passenden
  Automatik-Modus der Ventilsteuerung im Zeitraum vor Sonnenuntergang; veröffentlicht den Regen-Status
  zusätzlich stündlich als retained MQTT-Nachricht je Zone.
- Web-UI: neue Bewässerungs-Steuerung/-Anzeige (ajax/watering.php, js/solar/solarMQTT.js,
  assets/img/realtime.svg) inkl. Live-Status-Icons (aus/geplant/pausiert/aktiv per Farbe und Symbol).
- AutoAction-Regeleditor (ajax/actorDetails.php, sensorDetails.php, fillActorDD.php, fillSensorDD.php,
  tahoma.php, AutoAction.php) liest Actor-/Sensor-Parameter jetzt live aus der homeMesh-Datenbank statt
  aus stark vereinfachten Altfeldern, inkl. zugehöriger Anpassungen am Device-Discovery-Tooling
  (restricted/deviceDiscovery/*, neues logic_module.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 20:19:27 +02:00
admin 4de95c1a29 prevent alert if nothing is returned on modal form sent. 2026-02-14 20:36:43 +01:00
admin ae455dba20 modify filemode for linux 2026-02-14 20:08:34 +01:00
admin 0e78302640 Initial commit 2026-02-14 19:47:21 +01:00
admin fd2d3cee8b init 2026-02-02 19:21:09 +01:00
admin 715f90ca9b Initial commit 2026-02-02 18:20:19 +00:00