From b503483b51c40c6c81838437eab7c769d98fb305 Mon Sep 17 00:00:00 2001 From: estebanri87 <42171761+estebanri87@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:34:21 +0100 Subject: [PATCH 1/2] Enhance shading with interpolation (degree), UI, diagnostics, andshading standby status -Updated ETS/UI parameters for window orientation and azimuth selection; added compass/degree labels, clearer visibility rules, and azimuth sensor formatting. -Added ETS help text clarifying when aggregation is relevant versus interpolation. -Added diagnostic logging for ETS window orientation and azimuth target usage. -Introduced global per-channel Status Beschattung Bereit KO; removed per-mode readiness KO and adjusted KO offsets for shading/window-open modes. -Readiness logic now reflects user-influence factors: shading control enabled, no channel lock, no window-open handler, not in manual mode, and no shading lock/break lock active. -Implemented window open/tilt restore to previous shutter/slat position once the window closes. -Kept docs/scripts aligned (path fixes, generated docs) --- createDoc.ps1 | 3 +- ...likationsbeschreibung-ShutterController.md | 78 ++++ doc/Testplan-ShutterController.md | 131 ++++++ src/Baggages/Help_de/SHC-Azimut-Sensor-1.md | 5 + src/Baggages/Help_de/SHC-Azimut-Sensor-2.md | 5 + src/Baggages/Help_de/SHC-Azimut-Sensor-3.md | 5 + src/Baggages/Help_de/SHC-Azimut-Sensor-4.md | 5 + src/Baggages/Help_de/SHC-Azimut-Sensor-5.md | 5 + src/Baggages/Help_de/SHC-Azimut-auswerten.md | 6 + ...Behangausrichtung-und-Azimut-Auswertung.md | 18 + .../Help_de/SHC-Fenster-Behangausrichtung.md | 9 + .../SHC-FensterOffen-Aussperrverhinderung.md | 1 + .../Help_de/SHC-Helligkeit-Aggregation.md | 5 + .../Help_de/SHC-Helligkeit-Sensor-1.md | 5 + .../Help_de/SHC-Helligkeit-Sensor-2.md | 5 + .../Help_de/SHC-Helligkeit-Sensor-3.md | 5 + .../Help_de/SHC-Helligkeit-Sensor-4.md | 5 + .../Help_de/SHC-Helligkeit-Sensor-5.md | 5 + .../SHC-Weitere-Helligkeitssensoren.md | 5 + src/BrightnessMeasurement.cpp | 385 ++++++++++++++++++ src/BrightnessMeasurement.h | 79 ++++ src/CallContext.h | 18 +- src/MeasurementWatchdog.h | 26 +- src/ModeShading.cpp | 113 ++--- src/ModeShading.h | 3 +- src/PositionController.cpp | 18 +- src/ShutterControllerChannel.cpp | 207 ++++++++-- src/ShutterControllerChannel.h | 3 + src/ShutterControllerModule.ModeShading.xml | 38 +- ...ShutterControllerModule.ModeWindowOpen.xml | 2 +- src/ShutterControllerModule.cpp | 85 +++- src/ShutterControllerModule.h | 6 +- src/ShutterControllerModule.share.xml | 191 ++++++++- src/ShutterControllerModule.templ.xml | 46 ++- src/WindowOpenHandler.cpp | 12 +- test/README.md | 19 + test/brightness_logic_test.py | 149 +++++++ 37 files changed, 1539 insertions(+), 167 deletions(-) create mode 100644 doc/Testplan-ShutterController.md create mode 100644 src/Baggages/Help_de/SHC-Azimut-Sensor-1.md create mode 100644 src/Baggages/Help_de/SHC-Azimut-Sensor-2.md create mode 100644 src/Baggages/Help_de/SHC-Azimut-Sensor-3.md create mode 100644 src/Baggages/Help_de/SHC-Azimut-Sensor-4.md create mode 100644 src/Baggages/Help_de/SHC-Azimut-Sensor-5.md create mode 100644 src/Baggages/Help_de/SHC-Azimut-auswerten.md create mode 100644 src/Baggages/Help_de/SHC-Fenster-Behangausrichtung-und-Azimut-Auswertung.md create mode 100644 src/Baggages/Help_de/SHC-Fenster-Behangausrichtung.md create mode 100644 src/Baggages/Help_de/SHC-Helligkeit-Aggregation.md create mode 100644 src/Baggages/Help_de/SHC-Helligkeit-Sensor-1.md create mode 100644 src/Baggages/Help_de/SHC-Helligkeit-Sensor-2.md create mode 100644 src/Baggages/Help_de/SHC-Helligkeit-Sensor-3.md create mode 100644 src/Baggages/Help_de/SHC-Helligkeit-Sensor-4.md create mode 100644 src/Baggages/Help_de/SHC-Helligkeit-Sensor-5.md create mode 100644 src/Baggages/Help_de/SHC-Weitere-Helligkeitssensoren.md create mode 100644 src/BrightnessMeasurement.cpp create mode 100644 src/BrightnessMeasurement.h create mode 100644 test/README.md create mode 100644 test/brightness_logic_test.py diff --git a/createDoc.ps1 b/createDoc.ps1 index b4bdeaa..53f81ae 100644 --- a/createDoc.ps1 +++ b/createDoc.ps1 @@ -1 +1,2 @@ -OpenKNXproducer baggages -d doc/Applikationsbeschreibung-ShutterController.md -b src/Baggages/Help_de -p SHC \ No newline at end of file +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +OpenKNXproducer baggages -d "$scriptDir/doc/Applikationsbeschreibung-ShutterController.md" -b "$scriptDir/src/Baggages/Help_de" -p SHC \ No newline at end of file diff --git a/doc/Applikationsbeschreibung-ShutterController.md b/doc/Applikationsbeschreibung-ShutterController.md index 0a71f46..dc067a5 100644 --- a/doc/Applikationsbeschreibung-ShutterController.md +++ b/doc/Applikationsbeschreibung-ShutterController.md @@ -196,6 +196,67 @@ DOCCONTENT --> Vorgesehen für den Helligkeitswert einer KNX-Wetterstation. + +##### Weitere Helligkeitssensoren + +Legt fest, wie viele zusaetzliche Helligkeitssensoren (2 bis 5) verwendet werden. +Die Anzahl bestimmt, welche weiteren Kommunikationsobjekte sichtbar und zu verknuepfen sind. + + +##### Helligkeit Aggregation + +Bestimmt, wie mehrere gueltige Helligkeitssensoren zusammengefasst werden, wenn keine Azimut-Auswertung verwendet wird. +"Mittelwert" mittelt alle gueltigen Sensoren, "Maximum" nimmt den hoechsten Wert. + + +##### Fenster-/Behangausrichtung und Azimut-Auswertung + +Die Fenster-/Behangausrichtung im Kanal steuert, wie die Helligkeitssensoren ausgewertet werden: + +- **Ost/Suedost/Sued/Suedwest/West**: Azimut-Auswertung ist aktiv. Es werden nur Sensoren mit Azimut-Zuordnung verwendet. +- **Dachflaeche**: Bevorzugt Sensoren ohne Azimut-Zuordnung (z.B. Dachsensor). Falls keine vorhanden sind, wird der Maximalwert aller Sensoren verwendet. +- **Keine Himmelsrichtung (Azimut-Auswertung aus)**: Es wird die eingestellte Aggregation (Mittelwert/Maximum) ueber alle gueltigen Sensoren verwendet. + +Beispiele (vereinfachte Sicht): + +| Sensor-Setup | Fenster-/Behangausrichtung | Ergebnis fuer Helligkeit | +| --- | --- | --- | +| 3x Sensor mit Azimut (O/S/W) | Dachflaeche | Max(alle 3 Sensoren) | +| 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Dachflaeche | Max(alle Sensoren ohne Azimut) | +| 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Sued | Azimut-Interpolation nur mit den 4 Azimut-Sensoren | +| 2x Sensor ohne Azimut | Keine Himmelsrichtung (Auswertung aus) | Aggregation ueber alle Sensoren ohne Azimut | +| 1x Sensor mit Azimut | Keine Himmelsrichtung (Azimut-Auswertung aus) | Aggregation ueber alle gueltigen Sensoren | + + +##### Helligkeit Sensor 1 + +Azimut-Zuordnung fuer Sensor 1 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + + +##### Helligkeit Sensor 2 + +Azimut-Zuordnung fuer Sensor 2 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + + +##### Helligkeit Sensor 3 + +Azimut-Zuordnung fuer Sensor 3 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + + +##### Helligkeit Sensor 4 + +Azimut-Zuordnung fuer Sensor 4 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + + +##### Helligkeit Sensor 5 + +Azimut-Zuordnung fuer Sensor 5 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + #### UV-Index @@ -745,6 +806,23 @@ DOCCONTENT --> ### Beschattungssteuerung + +#### Azimut auswerten + +Wenn aktiviert, wird die aktuelle Sonnenrichtung fuer die Helligkeit verwendet. +Bei deaktivierter Azimut-Auswertung wird der Helligkeitswert aus der Aggregation der Sensoren gebildet. +Dies entspricht der Fenster-/Behangausrichtung "Keine Himmelsrichtung (Auswertung aus)". + + +#### Fenster-/Behangausrichtung + +Legt die Ausrichtung des Fensters bzw. Behangs fuer diesen Kanal fest. +Diese Angabe wird verwendet, um den passenden Helligkeitssensor fuer die Beschattungsauswertung auszuwählen. + +- **Ost/Suedost/Sued/Suedwest/West**: Azimut-Auswertung ist aktiv. Es werden nur Sensoren mit Azimut-Zuordnung verwendet. +- **Dachflaeche**: Bevorzugt Sensoren ohne Azimut-Zuordnung (z.B. Dachsensor). Falls keine vorhanden sind, wird der Maximalwert aller Sensoren verwendet. +- **Keine Himmelsrichtung (Azimut-Auswertung aus)**: Es wird die eingestellte Aggregation (Mittelwert/Maximum) ueber alle gueltigen Sensoren verwendet. + #### Nur starten wenn aktuelle Position kleiner gleich diff --git a/doc/Testplan-ShutterController.md b/doc/Testplan-ShutterController.md new file mode 100644 index 0000000..348d37d --- /dev/null +++ b/doc/Testplan-ShutterController.md @@ -0,0 +1,131 @@ +# Testplan ShutterController + +Dieser Testplan deckt Logik-, Integrations- und HIL/Manuell-Tests fuer die Beschattungslogik ab. +Ziel ist, die Auswertung von Helligkeit/Azimut, Prioritaeten der Modi, Fallback-Verhalten +sowie ETS/KNX-Integration sauber nachzuweisen. + +## 1) Automatisierte Logiktests (Unit) + +### 1.1 Helligkeit / Azimut / Dachflaeche +- TC-BRI-001: Azimut-Auswertung aktiv, 3 Sensoren mit Azimut (O/S/W), gueltiger Sonnenazimut. + - Erwartung: Azimut-Interpolation verwendet nur Sensoren mit Azimut. +- TC-BRI-002: Dachflaeche, 4 Sensoren mit Azimut + 1 Sensor ohne Azimut. + - Erwartung: Dachflaeche bevorzugt Sensor(en) ohne Azimut, nimmt Max der unzugeordneten. +- TC-BRI-003: Dachflaeche, nur Sensoren mit Azimut. + - Erwartung: Max ueber alle Sensoren. +- TC-BRI-004: Keine Himmelsrichtung (Azimut-Auswertung aus), Aggregation=Mean. + - Erwartung: Mittelwert aller gueltigen Sensoren. +- TC-BRI-005: Keine Himmelsrichtung (Azimut-Auswertung aus), Aggregation=Max. + - Erwartung: Max aller gueltigen Sensoren. +- TC-BRI-006: Sonnenazimut ungueltig. + - Erwartung: Azimut-Auswertung faellt auf Aggregatwert. +- TC-BRI-007: Sensor-Watchdog ignoreValue/waitForValue. + - Erwartung: Nicht-gueltige Sensoren werden nicht in Aggregat/Azimut genutzt. +- TC-BRI-008: Fallback-Mode Provide/Ignore. + - Erwartung: Fallback-Wert oder Ignorieren gemaess Einstellung. + +### 1.2 Modus-Prioritaeten +- TC-MOD-001: Fenster offen aktiv. + - Erwartung: Fenster-Modus hat hoehere Prioritaet als Handbetrieb/Nacht/Beschattung. +- TC-MOD-002: Handbetrieb aktiv, Fenster nicht aktiv. + - Erwartung: Handbetrieb vor Nacht/Beschattung. +- TC-MOD-003: Nachtmodus aktiv, keine Sperren. + - Erwartung: Nachtmodus vor Beschattung. +- TC-MOD-004: Mehrere Beschattungsmodi erlaubt. + - Erwartung: Hoechste erlaubte Nummer wird aktiv. + +### 1.3 Grenzen, Hysterese, Wartezeiten +- TC-LIM-001: Azimut/Elevation innerhalb Grenzen. + - Erwartung: Beschattung zulassbar. +- TC-LIM-002: Azimut/Elevation ausserhalb Grenzen. + - Erwartung: Beschattung nicht zulassbar. +- TC-LIM-003: Helligkeit Hysterese. + - Erwartung: Kein Flattern bei kleinen Schwankungen. +- TC-LIM-004: Beschattungsstart/Beschattungsende Wartezeiten. + - Erwartung: Aktivierung/Deaktivierung erst nach Ablauf. + +### 1.4 Fenster-Offen/Geoeffnet/GeKippt +- TC-WIN-001: Fensterkontakt 1 aktiv, 2 inaktiv. + - Erwartung: Fenster offen Modus. +- TC-WIN-002: Fensterkontakt 2 aktiv, je nach Konfiguration. + - Erwartung: Fenster gekippt Modus. +- TC-WIN-003: Restore vorheriger Position nach Fenster-Modus Ende. + - Erwartung: Position/Slat werden wiederhergestellt. + +### 1.5 Szenarien und Ergebnisse + +| TC | Szenario (Kurzform) | Automatisiert | Ergebnis | +| --- | --- | --- | --- | +| TC-BRI-001 | 2 Azimut-Sensoren (90/180), Sonnenazimut 135 -> Interpolation | Python | PASS | +| TC-BRI-002 | Dachflaeche, 2 Azimut + 1 ohne Azimut -> Max(ohne Azimut) | Python | PASS | +| TC-BRI-003 | Dachflaeche, nur Azimut-Sensoren -> Max(alle) | Python | PASS | +| TC-BRI-004 | Azimut-Auswertung aus, Aggregation=Mean | Nicht automatisiert | NICHT GETESTET | +| TC-BRI-005 | Azimut-Auswertung aus, Aggregation=Max | Nicht automatisiert | NICHT GETESTET | +| TC-BRI-006 | Sonnenazimut ungueltig -> Aggregatwert | Nicht automatisiert | NICHT GETESTET | +| TC-BRI-007 | Watchdog ignore/wait -> Sensoren ausgeschlossen | Nicht automatisiert | NICHT GETESTET | +| TC-BRI-008 | Fallback Provide/Ignore | Nicht automatisiert | NICHT GETESTET | +| TC-MOD-001 | Fenster offen aktiv -> Prioritaet hoch | Nicht automatisiert | NICHT GETESTET | +| TC-MOD-002 | Handbetrieb aktiv -> Prioritaet vor Nacht/Beschattung | Nicht automatisiert | NICHT GETESTET | +| TC-MOD-003 | Nachtmodus aktiv -> vor Beschattung | Nicht automatisiert | NICHT GETESTET | +| TC-MOD-004 | Mehrere Beschattungsmodi -> hoechste Nummer | Nicht automatisiert | NICHT GETESTET | +| TC-LIM-001 | Azimut/Elevation innerhalb Grenzen | Nicht automatisiert | NICHT GETESTET | +| TC-LIM-002 | Azimut/Elevation ausserhalb Grenzen | Nicht automatisiert | NICHT GETESTET | +| TC-LIM-003 | Helligkeit Hysterese | Nicht automatisiert | NICHT GETESTET | +| TC-LIM-004 | Wartezeiten Start/Ende | Nicht automatisiert | NICHT GETESTET | +| TC-WIN-001 | Kontakt 1 aktiv, Kontakt 2 inaktiv | Nicht automatisiert | NICHT GETESTET | +| TC-WIN-002 | Kontakt 2 aktiv (je nach Konfig) | Nicht automatisiert | NICHT GETESTET | +| TC-WIN-003 | Restore Position/Slat nach Fenster-Modus | Nicht automatisiert | NICHT GETESTET | + +Hinweise: +- PlatformIO C++ Testlauf konnte nicht gestartet werden ("pio" nicht verfuegbar, Python-Modul "platformio" nicht installiert). + +## 2) Simulation/Integration (teil-automatisiert) + +### 2.1 KO-Input-Simulation +- TC-SIM-001: Simuliere KO Eingaben fuer Helligkeit/Temp/Wolken/Regen. + - Erwartung: Beschattung erlaubt/nicht erlaubt gem. Grenzen. +- TC-SIM-002: Simuliere Fensterkontakte + Handbetrieb. + - Erwartung: Prioritaet Fenster > Hand > Nacht > Beschattung. + +### 2.2 Szenarien (End-to-End Logik) +- TC-SCEN-001: 4 Fassadensensoren + 1 Dachsensor. + - Erwartung: Fassadenkanaele nutzen Azimut, Dachflaeche nutzt Dachsensor. +- TC-SCEN-002: Nur Dachsensor, Dachflaeche. + - Erwartung: Dachsensor bestimmt Helligkeit. +- TC-SCEN-003: Nur Fassadensensoren, Dachflaeche. + - Erwartung: Max ueber alle Sensoren. +- TC-SCEN-004: Azimut-Auswertung aus. + - Erwartung: Aggregation ueber alle Sensoren. + +## 3) HIL / Manuell (ETS + Hardware) + +### 3.1 ETS/KNXprod +- TC-ETS-001: Import .knxprod, Seiten/Labels pruefen. + - Erwartung: "Keine Himmelsrichtung (Azimut-Auswertung aus)" sichtbar. +- TC-ETS-002: Help-Context Links. + - Erwartung: Alle Parameter zeigen passende Hilfe. + +### 3.2 Bus-Telegramme +- TC-BUS-001: Echte Gruppenadressen senden (Helligkeit/Temp/Lock). + - Erwartung: Beschattung reagiert gemaess Logik. +- TC-BUS-002: Fensteroeffnen/Kippen. + - Erwartung: Moduswechsel + Restore. + +### 3.3 Hardwareverhalten +- TC-HW-001: Positionsfahrt und Lamellenstellung. + - Erwartung: Werte korrekt angefahren. +- TC-HW-002: Timing/Watchdog. + - Erwartung: Fallback/Ignore Verhalten. + +## 4) Automatisierungs-Optionen + +- Unit-Tests als C++ Tests (z.B. in OFM-ShutterControllerModule/test). +- Python-Tests fuer Logik (fuer reine Berechnungen/Mocks). +- Simulation ueber Ko-Eingaben (CI-freundlich, keine Hardware noetig). + +## 5) Abnahmekriterien + +- Alle Unit-Tests gruen. +- Kritische Szenarien (Dachflaeche, Azimut-Auswertung aus) nachweislich korrekt. +- ETS-UI/Help-Context konsistent zur Doku. +- HIL-Tests erfolgreich fuer reale Telegramme und Fahrverhalten. diff --git a/src/Baggages/Help_de/SHC-Azimut-Sensor-1.md b/src/Baggages/Help_de/SHC-Azimut-Sensor-1.md new file mode 100644 index 0000000..7a5bff5 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Azimut-Sensor-1.md @@ -0,0 +1,5 @@ +### Azimut Sensor 1 + +Azimut-Zuordnung fuer Sensor 1 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Azimut-Sensor-2.md b/src/Baggages/Help_de/SHC-Azimut-Sensor-2.md new file mode 100644 index 0000000..ae32292 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Azimut-Sensor-2.md @@ -0,0 +1,5 @@ +### Azimut Sensor 2 + +Azimut-Zuordnung fuer Sensor 2 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Azimut-Sensor-3.md b/src/Baggages/Help_de/SHC-Azimut-Sensor-3.md new file mode 100644 index 0000000..78cf245 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Azimut-Sensor-3.md @@ -0,0 +1,5 @@ +### Azimut Sensor 3 + +Azimut-Zuordnung fuer Sensor 3 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Azimut-Sensor-4.md b/src/Baggages/Help_de/SHC-Azimut-Sensor-4.md new file mode 100644 index 0000000..71e5fb0 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Azimut-Sensor-4.md @@ -0,0 +1,5 @@ +### Azimut Sensor 4 + +Azimut-Zuordnung fuer Sensor 4 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Azimut-Sensor-5.md b/src/Baggages/Help_de/SHC-Azimut-Sensor-5.md new file mode 100644 index 0000000..be968e2 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Azimut-Sensor-5.md @@ -0,0 +1,5 @@ +### Azimut Sensor 5 + +Azimut-Zuordnung fuer Sensor 5 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Azimut-auswerten.md b/src/Baggages/Help_de/SHC-Azimut-auswerten.md new file mode 100644 index 0000000..9b0bf16 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Azimut-auswerten.md @@ -0,0 +1,6 @@ +### Azimut auswerten + +Wenn aktiviert, wird die aktuelle Sonnenrichtung fuer die Helligkeit verwendet. +Bei deaktivierter Azimut-Auswertung wird der Helligkeitswert aus der Aggregation der Sensoren gebildet. +Dies entspricht der Fenster-/Behangausrichtung "Keine Himmelsrichtung (Auswertung aus)". + diff --git a/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung-und-Azimut-Auswertung.md b/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung-und-Azimut-Auswertung.md new file mode 100644 index 0000000..96ae29a --- /dev/null +++ b/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung-und-Azimut-Auswertung.md @@ -0,0 +1,18 @@ +### Fenster-/Behangausrichtung und Azimut-Auswertung + +Die Fenster-/Behangausrichtung im Kanal steuert, wie die Helligkeitssensoren ausgewertet werden: + +- **Ost/Suedost/Sued/Suedwest/West**: Azimut-Auswertung ist aktiv. Es werden nur Sensoren mit Azimut-Zuordnung verwendet. +- **Dachflaeche**: Bevorzugt Sensoren ohne Azimut-Zuordnung (z.B. Dachsensor). Falls keine vorhanden sind, wird der Maximalwert aller Sensoren verwendet. +- **Keine Himmelsrichtung (Azimut-Auswertung aus)**: Es wird die eingestellte Aggregation (Mittelwert/Maximum) ueber alle gueltigen Sensoren verwendet. + +Beispiele (vereinfachte Sicht): + +| Sensor-Setup | Fenster-/Behangausrichtung | Ergebnis fuer Helligkeit | +| --- | --- | --- | +| 3x Sensor mit Azimut (O/S/W) | Dachflaeche | Max(alle 3 Sensoren) | +| 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Dachflaeche | Max(alle Sensoren ohne Azimut) | +| 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Sued | Azimut-Interpolation nur mit den 4 Azimut-Sensoren | +| 2x Sensor ohne Azimut | Keine Himmelsrichtung (Auswertung aus) | Aggregation ueber alle Sensoren ohne Azimut | +| 1x Sensor mit Azimut | Keine Himmelsrichtung (Azimut-Auswertung aus) | Aggregation ueber alle gueltigen Sensoren | + diff --git a/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung.md b/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung.md new file mode 100644 index 0000000..2f5d079 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung.md @@ -0,0 +1,9 @@ +### Fenster-/Behangausrichtung + +Legt die Ausrichtung des Fensters bzw. Behangs fuer diesen Kanal fest. +Diese Angabe wird verwendet, um den passenden Helligkeitssensor fuer die Beschattungsauswertung auszuwählen. + +- **Ost/Suedost/Sued/Suedwest/West**: Azimut-Auswertung ist aktiv. Es werden nur Sensoren mit Azimut-Zuordnung verwendet. +- **Dachflaeche**: Bevorzugt Sensoren ohne Azimut-Zuordnung (z.B. Dachsensor). Falls keine vorhanden sind, wird der Maximalwert aller Sensoren verwendet. +- **Keine Himmelsrichtung (Azimut-Auswertung aus)**: Es wird die eingestellte Aggregation (Mittelwert/Maximum) ueber alle gueltigen Sensoren verwendet. + diff --git a/src/Baggages/Help_de/SHC-FensterOffen-Aussperrverhinderung.md b/src/Baggages/Help_de/SHC-FensterOffen-Aussperrverhinderung.md index 74032dd..94d235d 100644 --- a/src/Baggages/Help_de/SHC-FensterOffen-Aussperrverhinderung.md +++ b/src/Baggages/Help_de/SHC-FensterOffen-Aussperrverhinderung.md @@ -6,3 +6,4 @@ Beispielanwendung: Dieser Wert wird verwendet um ein Aussperren auf einer Terrasse durch beginnende Beschattung zu verhindern. Werden hier Beispielsweise 20% eingestellt und die Terrassentüre ist vor dem Beginn der automatischen Beschattung geöffnet, wird die Jalousie zu maximal 20% geschlossen um ein Durchgehen noch zu ermöglichen. Erst nach dem Schließen der Terrassentüre wird die normale Beschattungsposition angefahren. + diff --git a/src/Baggages/Help_de/SHC-Helligkeit-Aggregation.md b/src/Baggages/Help_de/SHC-Helligkeit-Aggregation.md new file mode 100644 index 0000000..0bc9199 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Helligkeit-Aggregation.md @@ -0,0 +1,5 @@ +### Helligkeit Aggregation + +Bestimmt, wie mehrere gueltige Helligkeitssensoren zusammengefasst werden, wenn keine Azimut-Auswertung verwendet wird. +"Mittelwert" mittelt alle gueltigen Sensoren, "Maximum" nimmt den hoechsten Wert. + diff --git a/src/Baggages/Help_de/SHC-Helligkeit-Sensor-1.md b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-1.md new file mode 100644 index 0000000..a011bac --- /dev/null +++ b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-1.md @@ -0,0 +1,5 @@ +### Helligkeit Sensor 1 + +Azimut-Zuordnung fuer Sensor 1 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Helligkeit-Sensor-2.md b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-2.md new file mode 100644 index 0000000..cd2442b --- /dev/null +++ b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-2.md @@ -0,0 +1,5 @@ +### Helligkeit Sensor 2 + +Azimut-Zuordnung fuer Sensor 2 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Helligkeit-Sensor-3.md b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-3.md new file mode 100644 index 0000000..58719c4 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-3.md @@ -0,0 +1,5 @@ +### Helligkeit Sensor 3 + +Azimut-Zuordnung fuer Sensor 3 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Helligkeit-Sensor-4.md b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-4.md new file mode 100644 index 0000000..18bb31a --- /dev/null +++ b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-4.md @@ -0,0 +1,5 @@ +### Helligkeit Sensor 4 + +Azimut-Zuordnung fuer Sensor 4 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Helligkeit-Sensor-5.md b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-5.md new file mode 100644 index 0000000..42532cf --- /dev/null +++ b/src/Baggages/Help_de/SHC-Helligkeit-Sensor-5.md @@ -0,0 +1,5 @@ +### Helligkeit Sensor 5 + +Azimut-Zuordnung fuer Sensor 5 in 5-Grad-Schritten. +"Keine Zuordnung" deaktiviert die Azimut-Auswertung fuer diesen Sensor. + diff --git a/src/Baggages/Help_de/SHC-Weitere-Helligkeitssensoren.md b/src/Baggages/Help_de/SHC-Weitere-Helligkeitssensoren.md new file mode 100644 index 0000000..c5883d5 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Weitere-Helligkeitssensoren.md @@ -0,0 +1,5 @@ +### Weitere Helligkeitssensoren + +Legt fest, wie viele zusaetzliche Helligkeitssensoren (2 bis 5) verwendet werden. +Die Anzahl bestimmt, welche weiteren Kommunikationsobjekte sichtbar und zu verknuepfen sind. + diff --git a/src/BrightnessMeasurement.cpp b/src/BrightnessMeasurement.cpp new file mode 100644 index 0000000..c9f94e8 --- /dev/null +++ b/src/BrightnessMeasurement.cpp @@ -0,0 +1,385 @@ +#include "BrightnessMeasurement.h" +#include +#include + +void BrightnessMeasurement::init(const char* name, MeasurementWatchdogFallbackBehavior fallbackBehavior, float fallbackLux) +{ + _name = name; + _fallbackBehavior = fallbackBehavior; + _fallbackLux = fallbackLux; +} + +void BrightnessMeasurement::setSensors(const std::vector& sensors) +{ + _sensors = sensors; +} + +void BrightnessMeasurement::setAggregation(BrightnessAggregation aggregation) +{ + _aggregation = aggregation; +} + +void BrightnessMeasurement::setUseAzimuth(bool useAzimuth) +{ + if (_useAzimuth == useAzimuth) + return; + _useAzimuth = useAzimuth; + _aggregateStateMean.changed = true; + _aggregateStateMax.changed = true; + _azimuthState.changed = true; +} + +void BrightnessMeasurement::setAggregateUseMaxOverride(bool useMax) +{ + if (_aggregateUseMaxOverride == useMax) + return; + _aggregateUseMaxOverride = useMax; + _aggregateStateMean.changed = true; + _aggregateStateMax.changed = true; + _aggregateStateMeanUnassigned.changed = true; + _aggregateStateMaxUnassigned.changed = true; +} + +void BrightnessMeasurement::setAggregatePreferUnassigned(bool preferUnassigned) +{ + if (_aggregatePreferUnassigned == preferUnassigned) + return; + _aggregatePreferUnassigned = preferUnassigned; + _aggregateStateMean.changed = true; + _aggregateStateMax.changed = true; + _aggregateStateMeanUnassigned.changed = true; + _aggregateStateMaxUnassigned.changed = true; +} + +const std::string& BrightnessMeasurement::logPrefix() const +{ + return _name; +} + +KNXValue BrightnessMeasurement::getValue() const +{ + const auto& state = _useAzimuth ? _azimuthState : getAggregateState(); + return KNXValue(state.valueLux); +} + +bool BrightnessMeasurement::ignoreValue() const +{ + const auto& state = _useAzimuth ? _azimuthState : getAggregateState(); + return state.ignoreValue; +} + +bool BrightnessMeasurement::useFallback() const +{ + const auto& state = _useAzimuth ? _azimuthState : getAggregateState(); + return state.useFallback; +} + +bool BrightnessMeasurement::waitForValue() const +{ + const auto& state = _useAzimuth ? _azimuthState : getAggregateState(); + return state.waitForValue; +} + +bool BrightnessMeasurement::isChanged() const +{ + const auto& state = _useAzimuth ? _azimuthState : getAggregateState(); + return state.changed; +} + +bool BrightnessMeasurement::resetChanged() +{ + bool changed = _aggregateStateMean.changed || _aggregateStateMax.changed || + _aggregateStateMeanUnassigned.changed || _aggregateStateMaxUnassigned.changed || + _azimuthState.changed; + _aggregateStateMean.changed = false; + _aggregateStateMax.changed = false; + _aggregateStateMeanUnassigned.changed = false; + _aggregateStateMaxUnassigned.changed = false; + _azimuthState.changed = false; + return changed; +} + +void BrightnessMeasurement::logState(bool includeValue) const +{ + const auto& state = _useAzimuth ? _azimuthState : getAggregateState(); + logInfoP("State: %s", _useAzimuth ? "azimuth" : "aggregate"); + if (state.ignoreValue) + logInfoP("Value ignored"); + else if (state.useFallback) + logInfoP("Using fallback"); + if (includeValue && !state.ignoreValue) + logInfoP("Value: %lf", (double)state.valueLux); +} + +void BrightnessMeasurement::logSensorMapping(uint8_t channelIndex, bool useAzimuth) const +{ + size_t enabledCount = 0; + size_t azimuthCount = 0; + for (const auto& sensor : _sensors) + { + if (!sensor.enabled) + continue; + enabledCount++; + if (sensor.hasAzimuth) + azimuthCount++; + } + + const char* aggregationName = _aggregation == BrightnessAggregation::Max ? "max" : "mean"; + logInfoP("Brightness mapping CH%u: %s, aggregation=%s, sensors=%u, azimuths=%u", + (unsigned int)(channelIndex + 1), + useAzimuth ? "azimuth on" : "azimuth off", + aggregationName, + (unsigned int)enabledCount, + (unsigned int)azimuthCount); + + for (size_t i = 0; i < _sensors.size(); i++) + { + const auto& sensor = _sensors[i]; + if (!sensor.enabled) + { + logInfoP("Brightness sensor %u: disabled", (unsigned int)(i + 1)); + continue; + } + if (sensor.hasAzimuth) + logInfoP("Brightness sensor %u: enabled, azimuth=%u", (unsigned int)(i + 1), (unsigned int)sensor.azimuth); + else + logInfoP("Brightness sensor %u: enabled, azimuth=none", (unsigned int)(i + 1)); + } +} + +void BrightnessMeasurement::update(unsigned long currentMillis, bool diagnosticLog, float sunAzimuth, bool sunAzimuthValid) +{ + (void)currentMillis; + (void)diagnosticLog; + + _anySensorChanged = false; + for (const auto& sensor : _sensors) + { + if (sensor.watchdog != nullptr && sensor.watchdog->isChanged()) + _anySensorChanged = true; + } + + auto aggregateStatesAll = buildAggregateStates(false); + auto aggregateStatesUnassigned = buildAggregateStates(true); + _aggregateUnassignedCount = aggregateStatesUnassigned.count; + + const auto& aggregateStateForAzimuth = + (_aggregation == BrightnessAggregation::Max) ? aggregateStatesAll.max : aggregateStatesAll.mean; + auto azimuthState = buildAzimuthState(sunAzimuth, sunAzimuthValid, aggregateStateForAzimuth); + + _aggregateStateMean.changed = _anySensorChanged || + std::fabs(_aggregateStateMean.valueLux - aggregateStatesAll.mean.valueLux) > 0.001f || + _aggregateStateMean.ignoreValue != aggregateStatesAll.mean.ignoreValue || + _aggregateStateMean.useFallback != aggregateStatesAll.mean.useFallback || + _aggregateStateMean.waitForValue != aggregateStatesAll.mean.waitForValue; + + _aggregateStateMax.changed = _anySensorChanged || + std::fabs(_aggregateStateMax.valueLux - aggregateStatesAll.max.valueLux) > 0.001f || + _aggregateStateMax.ignoreValue != aggregateStatesAll.max.ignoreValue || + _aggregateStateMax.useFallback != aggregateStatesAll.max.useFallback || + _aggregateStateMax.waitForValue != aggregateStatesAll.max.waitForValue; + + _aggregateStateMeanUnassigned.changed = _anySensorChanged || + std::fabs(_aggregateStateMeanUnassigned.valueLux - aggregateStatesUnassigned.mean.valueLux) > 0.001f || + _aggregateStateMeanUnassigned.ignoreValue != aggregateStatesUnassigned.mean.ignoreValue || + _aggregateStateMeanUnassigned.useFallback != aggregateStatesUnassigned.mean.useFallback || + _aggregateStateMeanUnassigned.waitForValue != aggregateStatesUnassigned.mean.waitForValue; + + _aggregateStateMaxUnassigned.changed = _anySensorChanged || + std::fabs(_aggregateStateMaxUnassigned.valueLux - aggregateStatesUnassigned.max.valueLux) > 0.001f || + _aggregateStateMaxUnassigned.ignoreValue != aggregateStatesUnassigned.max.ignoreValue || + _aggregateStateMaxUnassigned.useFallback != aggregateStatesUnassigned.max.useFallback || + _aggregateStateMaxUnassigned.waitForValue != aggregateStatesUnassigned.max.waitForValue; + + _azimuthState.changed = _anySensorChanged || + std::fabs(_azimuthState.valueLux - azimuthState.valueLux) > 0.001f || + _azimuthState.ignoreValue != azimuthState.ignoreValue || + _azimuthState.useFallback != azimuthState.useFallback || + _azimuthState.waitForValue != azimuthState.waitForValue; + + _aggregateStateMean = aggregateStatesAll.mean; + _aggregateStateMax = aggregateStatesAll.max; + _aggregateStateMeanUnassigned = aggregateStatesUnassigned.mean; + _aggregateStateMaxUnassigned = aggregateStatesUnassigned.max; + _azimuthState = azimuthState; +} + +const BrightnessMeasurement::ValueState& BrightnessMeasurement::getAggregateState() const +{ + const bool useUnassigned = _aggregatePreferUnassigned && _aggregateUnassignedCount > 0; + const auto& meanState = useUnassigned ? _aggregateStateMeanUnassigned : _aggregateStateMean; + const auto& maxState = useUnassigned ? _aggregateStateMaxUnassigned : _aggregateStateMax; + if (_aggregateUseMaxOverride || _aggregation == BrightnessAggregation::Max) + return maxState; + return meanState; +} + +BrightnessMeasurement::AggregateStates BrightnessMeasurement::buildAggregateStates(bool onlyUnassigned) const +{ + AggregateStates states; + float sum = 0.0f; + float maxValue = 0.0f; + size_t count = 0; + + for (const auto& sensor : _sensors) + { + if (!isSensorValid(sensor)) + continue; + if (onlyUnassigned && sensor.hasAzimuth) + continue; + float value = (float)sensor.watchdog->getValue(); + sum += value; + if (count == 0 || value > maxValue) + maxValue = value; + count++; + } + states.count = count; + + if (count > 0) + { + states.mean.ignoreValue = false; + states.mean.useFallback = false; + states.mean.waitForValue = false; + states.mean.valueLux = sum / (float)count; + + states.max.ignoreValue = false; + states.max.useFallback = false; + states.max.waitForValue = false; + states.max.valueLux = maxValue; + return states; + } + + states.mean.waitForValue = true; + states.max.waitForValue = true; + switch (_fallbackBehavior) + { + case MeasurementWatchdogFallbackBehavior::ProvideFallbackValue: + case MeasurementWatchdogFallbackBehavior::RequestValueAndProvideFallbackValue: + states.mean.ignoreValue = false; + states.mean.useFallback = true; + states.mean.valueLux = _fallbackLux; + + states.max.ignoreValue = false; + states.max.useFallback = true; + states.max.valueLux = _fallbackLux; + break; + case MeasurementWatchdogFallbackBehavior::IgnoreValue: + case MeasurementWatchdogFallbackBehavior::RequestValueAndIgnore: + default: + states.mean.ignoreValue = true; + states.mean.useFallback = false; + + states.max.ignoreValue = true; + states.max.useFallback = false; + break; + } + return states; +} + +BrightnessMeasurement::ValueState BrightnessMeasurement::buildAzimuthState(float sunAzimuth, bool sunAzimuthValid, const ValueState& aggregateState) const +{ + ValueState state; + + struct AzimuthValue + { + float azimuth = 0.0f; + float valueLux = 0.0f; + }; + + std::vector values; + for (const auto& sensor : _sensors) + { + if (!isSensorValid(sensor) || !sensor.hasAzimuth) + continue; + values.push_back({(float)sensor.azimuth, (float)sensor.watchdog->getValue()}); + } + + if (!sunAzimuthValid) + values.clear(); + + if (values.empty()) + { + return aggregateState; + } + + std::sort(values.begin(), values.end(), [](const AzimuthValue& left, const AzimuthValue& right) + { + return left.azimuth < right.azimuth; + }); + + std::vector merged; + for (const auto& entry : values) + { + if (!merged.empty() && std::fabs(merged.back().azimuth - entry.azimuth) < 0.001f) + { + merged.back().valueLux = (merged.back().valueLux + entry.valueLux) / 2.0f; + continue; + } + merged.push_back(entry); + } + + if (merged.size() == 1) + { + state.ignoreValue = false; + state.useFallback = false; + state.waitForValue = false; + state.valueLux = merged.front().valueLux; + return state; + } + + float target = normalizeAzimuth(sunAzimuth); + float result = merged.front().valueLux; + + for (size_t i = 0; i < merged.size(); i++) + { + float a0 = merged[i].azimuth; + float a1 = merged[(i + 1) % merged.size()].azimuth; + float v0 = merged[i].valueLux; + float v1 = merged[(i + 1) % merged.size()].valueLux; + float segmentStart = a0; + float segmentEnd = a1; + + if (i == merged.size() - 1) + segmentEnd += 360.0f; + + float targetWrapped = target; + if (targetWrapped < segmentStart) + targetWrapped += 360.0f; + + if (targetWrapped >= segmentStart && targetWrapped <= segmentEnd) + { + float ratio = (segmentEnd - segmentStart) > 0.0f ? + (targetWrapped - segmentStart) / (segmentEnd - segmentStart) : 0.0f; + result = v0 + (v1 - v0) * ratio; + break; + } + } + + state.ignoreValue = false; + state.useFallback = false; + state.waitForValue = false; + state.valueLux = result; + return state; +} + +float BrightnessMeasurement::normalizeAzimuth(float azimuth) +{ + while (azimuth < 0.0f) + azimuth += 360.0f; + while (azimuth >= 360.0f) + azimuth -= 360.0f; + return azimuth; +} + +bool BrightnessMeasurement::isSensorValid(const BrightnessSensor& sensor) +{ + if (sensor.watchdog == nullptr) + return false; + if (!sensor.enabled) + return false; + if (sensor.watchdog->ignoreValue()) + return false; + if (sensor.watchdog->waitForValue()) + return false; + return true; +} diff --git a/src/BrightnessMeasurement.h b/src/BrightnessMeasurement.h new file mode 100644 index 0000000..2435c08 --- /dev/null +++ b/src/BrightnessMeasurement.h @@ -0,0 +1,79 @@ +#pragma once +#include "MeasurementWatchdog.h" +#include + +enum class BrightnessAggregation : uint8_t +{ + Mean = 0, + Max = 1 +}; + +struct BrightnessSensor +{ + const MeasurementWatchdog* watchdog = nullptr; + uint16_t azimuth = 0; + bool hasAzimuth = false; + bool enabled = false; +}; + +class BrightnessMeasurement : public MeasurementSource +{ +public: + void init(const char* name, MeasurementWatchdogFallbackBehavior fallbackBehavior, float fallbackLux); + void setSensors(const std::vector& sensors); + void setAggregation(BrightnessAggregation aggregation); + void update(unsigned long currentMillis, bool diagnosticLog, float sunAzimuth, bool sunAzimuthValid); + void setUseAzimuth(bool useAzimuth); + void setAggregateUseMaxOverride(bool useMax); + void setAggregatePreferUnassigned(bool preferUnassigned); + + const std::string& logPrefix() const override; + KNXValue getValue() const override; + bool ignoreValue() const override; + bool useFallback() const override; + bool waitForValue() const override; + bool isChanged() const override; + + bool resetChanged(); + void logState(bool includeValue) const; + void logSensorMapping(uint8_t channelIndex, bool useAzimuth) const; + +private: + struct ValueState + { + float valueLux = 0.0f; + bool ignoreValue = true; + bool useFallback = false; + bool waitForValue = true; + bool changed = false; + }; + + std::string _name; + std::vector _sensors; + MeasurementWatchdogFallbackBehavior _fallbackBehavior = MeasurementWatchdogFallbackBehavior::IgnoreValue; + float _fallbackLux = 0.0f; + BrightnessAggregation _aggregation = BrightnessAggregation::Mean; + bool _useAzimuth = true; + bool _aggregateUseMaxOverride = false; + bool _aggregatePreferUnassigned = false; + size_t _aggregateUnassignedCount = 0; + ValueState _aggregateStateMean; + ValueState _aggregateStateMax; + ValueState _aggregateStateMeanUnassigned; + ValueState _aggregateStateMaxUnassigned; + ValueState _azimuthState; + bool _anySensorChanged = false; + + struct AggregateStates + { + ValueState mean; + ValueState max; + size_t count = 0; + }; + + const ValueState& getAggregateState() const; + AggregateStates buildAggregateStates(bool onlyUnassigned) const; + ValueState buildAzimuthState(float sunAzimuth, bool sunAzimuthValid, const ValueState& aggregateState) const; + static float normalizeAzimuth(float azimuth); + static bool isSensorValid(const BrightnessSensor& sensor); +}; diff --git a/src/CallContext.h b/src/CallContext.h index d1cd823..1d6072f 100644 --- a/src/CallContext.h +++ b/src/CallContext.h @@ -6,7 +6,7 @@ class ModeIdle; class ModeManual; class ModeBase; -class MeasurementWatchdog; +class MeasurementSource; class PositionController; class CallContext @@ -38,13 +38,13 @@ class CallContext const ModeIdle* modeIdle = nullptr; const ModeManual* modeManual = nullptr; const ModeBase* modeCurrentActive = nullptr; - MeasurementWatchdog* measurementTemperature = nullptr; - MeasurementWatchdog* measurementTemperatureForecast = nullptr; - MeasurementWatchdog* measurementBrightness = nullptr; - MeasurementWatchdog* measurementUVIndex = nullptr; - MeasurementWatchdog* measurementRain = nullptr; - MeasurementWatchdog* measurementClouds = nullptr; - MeasurementWatchdog* measurementRoomTemperature = nullptr; - MeasurementWatchdog* measurementHeading = nullptr; + MeasurementSource* measurementTemperature = nullptr; + MeasurementSource* measurementTemperatureForecast = nullptr; + MeasurementSource* measurementBrightness = nullptr; + MeasurementSource* measurementUVIndex = nullptr; + MeasurementSource* measurementRain = nullptr; + MeasurementSource* measurementClouds = nullptr; + MeasurementSource* measurementRoomTemperature = nullptr; + MeasurementSource* measurementHeading = nullptr; const PositionController* positionController = nullptr; }; \ No newline at end of file diff --git a/src/MeasurementWatchdog.h b/src/MeasurementWatchdog.h index af32e88..adf6d03 100644 --- a/src/MeasurementWatchdog.h +++ b/src/MeasurementWatchdog.h @@ -24,7 +24,19 @@ enum MeasurementWatchdogFallbackBehavior : uint8_t RequestValueAndProvideFallbackValue = 3 }; -class MeasurementWatchdog +class MeasurementSource +{ +public: + virtual ~MeasurementSource() = default; + virtual const std::string& logPrefix() const = 0; + virtual KNXValue getValue() const = 0; + virtual bool ignoreValue() const = 0; + virtual bool useFallback() const = 0; + virtual bool waitForValue() const = 0; + virtual bool isChanged() const = 0; +}; + +class MeasurementWatchdog : public MeasurementSource { private: const static unsigned long _waitForValueTimeout = 10000; @@ -43,16 +55,16 @@ class MeasurementWatchdog static void resetMissingValue(); static bool missingValue(); MeasurementWatchdog(); - const std::string& logPrefix() const; + const std::string& logPrefix() const override; void init(const char* name, GroupObject* groupObject, uint8_t timeoutParameterValue, const KNXValue& fallbackValue, const Dpt& dpt, MeasurementWatchdogFallbackBehavior fallbackBehaviour); void setup(); void update(unsigned long currentMillis, bool diagnosticLog); - KNXValue getValue() const; - bool ignoreValue() const; - bool useFallback() const; - bool waitForValue() const; + KNXValue getValue() const override; + bool ignoreValue() const override; + bool useFallback() const override; + bool waitForValue() const override; void processIputKo(GroupObject& go); - bool isChanged() const; + bool isChanged() const override; bool resetChanged(); void logState(bool incudeValue); }; \ No newline at end of file diff --git a/src/ModeShading.cpp b/src/ModeShading.cpp index b4f7b74..18c8248 100644 --- a/src/ModeShading.cpp +++ b/src/ModeShading.cpp @@ -25,8 +25,6 @@ ModeShading::ModeShading(uint8_t index) _name = "Shading"; _name += std::to_string(index); logInfoP("ModeShading %s created", _name.c_str()); - - } const char *ModeShading::name() const @@ -160,7 +158,7 @@ bool ModeShading::allowed(const CallContext &callContext) { logDebugP("Allowed by sun: %d", (int)allowedSun); _lastSunFrameAllowed = allowedSun; - _needWaitTime = false; // shading peruiod changed, no wait time needed + _needWaitTime = false; // shading period changed, no wait time needed } bool logWaitTimeResult = false; if (_recalcMeasurmentValues || callContext.diagnosticLog) @@ -172,19 +170,19 @@ bool ModeShading::allowed(const CallContext &callContext) bool allowedByHeatingOff = true; if (_waitTimeAfterHeatingValueChange != 0) { - // - // - // - // - // - // - // - // - // - // - // - // - // + // + // + // + // + // + // + // + // + // + // + // + // + // unsigned long waitTimeInMillis = 0; switch (ParamSHC_CShading1HeatingActive) { @@ -250,21 +248,21 @@ bool ModeShading::allowed(const CallContext &callContext) allowedByHeatingOff = false; } } - // Check if allow changed through measurment values and heating off + // Check if allowance changed through measurement values and heating off bool allowedByMeasurementValuesAndHeatingOffWaitTime = _allowedByMeasurementValues && allowedByHeatingOff; if (_allowedByMeasurementValuesAndHeatingOffWaitTime != allowedByMeasurementValuesAndHeatingOffWaitTime) { _allowedByMeasurementValuesAndHeatingOffWaitTime = allowedByMeasurementValuesAndHeatingOffWaitTime; if (_active && !allowedByMeasurementValuesAndHeatingOffWaitTime) - _needWaitTime = true; // not longer allowed and active, active wait time for deactivation and reactivation + _needWaitTime = true; // no longer allowed and active, start wait time for deactivation and reactivation if (_active) { if (_needWaitTime && !allowedByMeasurementValuesAndHeatingOffWaitTime) { logDebugP("Start stopping wait time"); - _waitTimeAfterMeasurmentValueChange = callContext.currentMillis; // acivate stop wait time + _waitTimeAfterMeasurmentValueChange = callContext.currentMillis; // activate stop wait time } else { @@ -353,6 +351,11 @@ bool ModeShading::allowed(const CallContext &callContext) _lastNotAllowedReason = _notAllowedReason; updateDiagnosticKos(); } +#ifdef KoSHC_CShading1Ready + bool readiness = _notAllowedReason == 0; + if (KoSHC_CShading1Ready.valueNoSendCompare(readiness, DPT_Switch)) + KoSHC_CShading1Ready.objectWritten(); +#endif // Return result if (!_lastSunFrameAllowed) return false; @@ -402,11 +405,11 @@ bool ModeShading::allowedBySun(const CallContext &callContext) { auto shadingBreak = ParamSHC_CShading1Break; - // - // - // - // - // + // + // + // + // + // switch (shadingBreak) { case 1: @@ -456,35 +459,35 @@ bool ModeShading::allowedBySun(const CallContext &callContext) return allowed; } -bool ModeShading::handleMeasurmentValue(bool &allowed, bool enabled, const MeasurementWatchdog *measurementWatchdog, const CallContext &callContext, bool (*predicate)(const MeasurementWatchdog *, uint8_t _channelIndex, uint8_t _index, bool previousAllowed), ModeShadingNotAllowedReason reasonBit) +bool ModeShading::handleMeasurmentValue(bool &allowed, bool enabled, const MeasurementSource *measurementSource, const CallContext &callContext, bool (*predicate)(const MeasurementSource *, uint8_t _channelIndex, uint8_t _index, bool previousAllowed), ModeShadingNotAllowedReason reasonBit) { if (!enabled) { _notAllowedReason &= ~reasonBit; return true; } - if (measurementWatchdog->ignoreValue()) + if (measurementSource->ignoreValue()) { if (callContext.diagnosticLog) - logInfoP("%s: value ignore", measurementWatchdog->logPrefix().c_str()); + logInfoP("%s: value ignore", measurementSource->logPrefix().c_str()); _notAllowedReason &= ~reasonBit; return true; } - if (measurementWatchdog->waitForValue()) + if (measurementSource->waitForValue()) { if (callContext.diagnosticLog) - logInfoP("%s: wait for value", measurementWatchdog->logPrefix().c_str()); + logInfoP("%s: wait for value", measurementSource->logPrefix().c_str()); _notAllowedReason |= reasonBit; allowed = false; return true; } - bool previousAllowed = !measurementWatchdog->useFallback() && !(_notAllowedReason & reasonBit); + bool previousAllowed = !measurementSource->useFallback() && !(_notAllowedReason & reasonBit); - if (!predicate(measurementWatchdog, _channelIndex, _index, previousAllowed)) + if (!predicate(measurementSource, _channelIndex, _index, previousAllowed)) { if (callContext.diagnosticLog) - logInfoP("%s: value not allowed", measurementWatchdog->logPrefix().c_str()); + logInfoP("%s: value not allowed", measurementSource->logPrefix().c_str()); allowed = false; _notAllowedReason |= reasonBit; return false; @@ -504,7 +507,7 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) ParamSHC_CShading1TempActive, callContext.measurementTemperature, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return (float)m->getValue() >= ParamSHC_CShading1TempMin; }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonTemperature); @@ -513,7 +516,7 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) ParamSHC_CShading1TempForecastActive, callContext.measurementTemperatureForecast, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return (float)m->getValue() >= ParamSHC_CShading1TempForecastMin; }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonTemperatureForecase); @@ -522,7 +525,7 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) ParamSHC_CShading1BrightnessActive, callContext.measurementBrightness, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return (double)m->getValue() >= max(0., 1000. * ((double) ParamSHC_CShading1BrightnessMin) - (previousAllowed ? ((double) ParamSHC_CShading1BrightnessHyst) * 1000. : 0)); }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonBrightness); @@ -531,7 +534,7 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) ParamSHC_CShading1UVIActive, callContext.measurementUVIndex, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return (float)m->getValue() >= ParamSHC_CShading1UVIMin; }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonUVI); @@ -540,7 +543,7 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) ParamSHC_CShading1RainActive, callContext.measurementRain, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return !(bool)m->getValue(); }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonRain); @@ -549,7 +552,7 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) ParamSHC_CShading1Clouds != 101, callContext.measurementClouds, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return (uint8_t)m->getValue() <= ParamSHC_CShading1Clouds; }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonClouds); @@ -558,22 +561,22 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) ParamSHC_CShading1RoomTemperaturActive, callContext.measurementRoomTemperature, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return (float)m->getValue() >= ParamSHC_CRoomTemp; }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonRoomTemperature); - // - // - // + // + // + // bool heatingOff; if (ParamSHC_CHeatingInput == 1) { heatingOff = handleMeasurmentValue( allowed, - ParamSHC_CShading1HeatingActive != 0, // + ParamSHC_CShading1HeatingActive != 0, // callContext.measurementHeading, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return (uint8_t)m->getValue() <= ParamSHC_CShading1MaxHeatingValue; }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonHeating); } @@ -581,10 +584,10 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) { heatingOff = handleMeasurmentValue( allowed, - ParamSHC_CShading1HeatingActive != 0, // + ParamSHC_CShading1HeatingActive != 0, // callContext.measurementHeading, callContext, - [](const MeasurementWatchdog *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) + [](const MeasurementSource *m, uint8_t _channelIndex, uint8_t _index, bool previousAllowed) { return !(bool)m->getValue(); }, ModeShadingNotAllowedReason::ModeShadingNotAllowedReasonHeating); if (callContext.measurementHeading->waitForValue()) @@ -625,9 +628,9 @@ void ModeShading::start(const CallContext &callContext, const ModeBase *previous KoSHC_CShading1Active.value(true, DPT_Switch); positionController.setAutomaticPosition(ParamSHC_CShading1ShadingPosition); - // - // - // + // + // + // if (!ParamSHC_CShading1SlatElevationDepending) positionController.setAutomaticSlat(ParamSHC_CShading1SlatShadingPosition); } @@ -637,9 +640,9 @@ void ModeShading::control(const CallContext &callContext, PositionController &po if (!callContext.modeNewStarted && !callContext.minuteChanged && !callContext.diagnosticLog) return; - // - // - // + // + // + // if (!positionController.hasSlat()) return; @@ -664,7 +667,7 @@ void ModeShading::control(const CallContext &callContext, PositionController &po if (callContext.diagnosticLog) logInfoP("Slat position %d difference is less then %d", (int)abs((uint8_t)KoSHC_CShutterSlatOutput.value(DPT_Scaling) - slatPosition), (int)ParamSHC_CShading1MinChangeForSlatAdaption); - return; // Do not change, to less difference + return; // Do not change, too little difference } positionController.setAutomaticSlat(slatPosition); } @@ -701,11 +704,11 @@ void ModeShading::processInputKo(GroupObject &ko, PositionController &positionCo KoSHC_CShading1BreakLockActive.value(_breakLockActive, DPT_Switch); return; } - // global ko + // global KO switch (ko.asap()) { case SHC_KoCShadingControl: - // Manual activativation / deativiation stop wait time + // Manual activation / deactivation stops wait time _waitTimeAfterMeasurmentValueChange = 0; return; } diff --git a/src/ModeShading.h b/src/ModeShading.h index 2b0cf48..0917411 100644 --- a/src/ModeShading.h +++ b/src/ModeShading.h @@ -1,6 +1,7 @@ #pragma once #include "ModeBase.h" +class MeasurementSource; enum ModeShadingNotAllowedReason : uint32_t { @@ -50,7 +51,7 @@ class ModeShading : public ModeBase unsigned long _lastHeadingTimeStamp = 0; bool _heatingOff = true; bool allowedByMeasurmentValues(const CallContext& callContext); - bool handleMeasurmentValue(bool& allowed, bool enabled, const MeasurementWatchdog *measurementWatchdog, const CallContext &callContext, bool (*predicate)(const MeasurementWatchdog *, uint8_t _channelIndex, uint8_t _index, bool previousAllowed), ModeShadingNotAllowedReason reasonBit); + bool handleMeasurmentValue(bool& allowed, bool enabled, const MeasurementSource *measurementSource, const CallContext &callContext, bool (*predicate)(const MeasurementSource *, uint8_t _channelIndex, uint8_t _index, bool previousAllowed), ModeShadingNotAllowedReason reasonBit); void updateDiagnosticKos(); public: ModeShading(uint8_t index); diff --git a/src/PositionController.cpp b/src/PositionController.cpp index 82e1f61..8dc69bf 100644 --- a/src/PositionController.cpp +++ b/src/PositionController.cpp @@ -91,9 +91,9 @@ void PositionController::setManualPosition(uint8_t manualPosition) logDebugP("Set manual position: %d", (int)manualPosition); _restorePosition = manualPosition; - // - // - // + // + // + // if (ParamSHC_CManualUpDownType != 0) { setAutomaticPosition(manualPosition); @@ -115,9 +115,9 @@ void PositionController::setManualSlat(uint8_t manualSlat) logDebugP("Set manual slat position: %d", (int)manualSlat); _restoreSlat = manualSlat; - // - // - // + // + // + // if (_hasSlat && (ParamSHC_CManualUpDownType != 0)) setAutomaticSlat(manualSlat); } @@ -168,9 +168,9 @@ void PositionController::setManualUpDown(bool down) _calculatedTargetPosition = _restorePosition; setMovingTimeout(MOVING_TIMEOUT); } - // - // - // + // + // + // switch (ParamSHC_CManualUpDownType) { case 1: diff --git a/src/ShutterControllerChannel.cpp b/src/ShutterControllerChannel.cpp index 83bfd05..4fcff77 100755 --- a/src/ShutterControllerChannel.cpp +++ b/src/ShutterControllerChannel.cpp @@ -3,9 +3,43 @@ #include "WindowOpenHandler.h" #include "ModeNight.h" #include "ModeShading.h" +#include "BrightnessMeasurement.h" #include "ModeIdle.h" #include "ShutterSimulation.h" +namespace +{ + struct WindowOrientationInfo + { + const char* name; + uint16_t azimuth; + bool hasAzimuth; + }; + + WindowOrientationInfo getWindowOrientationInfo(uint8_t value) + { + switch (value) + { + case 0: + return {"Ost", 90, true}; + case 1: + return {"Suedost", 135, true}; + case 2: + return {"Sued", 180, true}; + case 3: + return {"Suedwest", 225, true}; + case 4: + return {"West", 270, true}; + case 5: + return {"Dachflaeche", 0, false}; + case 6: + return {"Keine Himmelsrichtung (Azimut-Auswertung aus)", 0, false}; + default: + return {"Unbekannt", 0, false}; + } + } +} + ShutterControllerChannel::ShutterControllerChannel(uint8_t channelIndex) : _modes(), _windowOpenHandlers(), _positionController(channelIndex) { @@ -28,9 +62,9 @@ void ShutterControllerChannel::setup() DPT_Value_Temp, (MeasurementWatchdogFallbackBehavior)ParamSHC_CRoomTempWatchdogBehavior); - // - // - // + // + // + // _measurementHeading.init( "Heading", ParamSHC_CHeatingInput > 0 ? &KoSHC_CHeading : nullptr, @@ -99,7 +133,7 @@ void ShutterControllerChannel::processInputKo(GroupObject &ko) _measurementRoomTemperature.processIputKo(ko); - // channel ko + // channel KO auto index = SHC_KoCalcIndex(ko.asap()); switch (index) { @@ -316,25 +350,25 @@ void ShutterControllerChannel::activateShading() unsigned long ShutterControllerChannel::getManualShadingWaitTimeInMs() const { - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // auto value = ParamSHC_CManualShadingWaitTime; if (value < 100) return value * 60 * 1000; @@ -343,6 +377,41 @@ unsigned long ShutterControllerChannel::getManualShadingWaitTimeInMs() const void ShutterControllerChannel::execute(CallContext &callContext) { + if (callContext.measurementBrightness != nullptr) + { + auto brightnessMeasurement = static_cast(callContext.measurementBrightness); + const auto orientationInfo = getWindowOrientationInfo(ParamSHC_CWindowOrientation); + const bool useAzimuth = ParamSHC_CBrightnessAzimuthEnabled != 0 && orientationInfo.hasAzimuth; + const bool preferUnassigned = ParamSHC_CWindowOrientation == 5; + const bool forceAggregateMax = ParamSHC_CWindowOrientation == 5; + brightnessMeasurement->setUseAzimuth(useAzimuth); + brightnessMeasurement->setAggregateUseMaxOverride(forceAggregateMax); + brightnessMeasurement->setAggregatePreferUnassigned(preferUnassigned); + if (callContext.diagnosticLog) + { + if (orientationInfo.hasAzimuth) + logInfoP("Brightness CH%u: window orientation=%s (%u)", + (unsigned int)(_channelIndex + 1), + orientationInfo.name, + (unsigned int)orientationInfo.azimuth); + else + logInfoP("Brightness CH%u: window orientation=%s", + (unsigned int)(_channelIndex + 1), + orientationInfo.name); + + if (useAzimuth) + logInfoP("Brightness CH%u: target azimuth(sun)=%.2f (%s)", + (unsigned int)(_channelIndex + 1), + callContext.azimuth, + callContext.timeAndSunValid ? "valid" : "invalid"); + else + logInfoP("Brightness CH%u: target=aggregate (no azimuth)", + (unsigned int)(_channelIndex + 1)); + + brightnessMeasurement->logSensorMapping(_channelIndex, useAzimuth); + } + } + _measurementHeading.update(callContext.currentMillis, callContext.diagnosticLog); _measurementRoomTemperature.update(callContext.currentMillis, callContext.diagnosticLog); @@ -367,7 +436,7 @@ void ShutterControllerChannel::execute(CallContext &callContext) } callContext.modeCurrentActive = _currentMode; - // Handle reacticvate of shading after manual usage + // Handle reactivation of shading after manual usage if (_waitTimeForReactivateShadingAfterManualStarted != 0) { auto waitTime = callContext.fastSimulationActive ? getManualShadingWaitTimeInMs() / 10 : getManualShadingWaitTimeInMs(); @@ -408,14 +477,14 @@ void ShutterControllerChannel::execute(CallContext &callContext) WindowOpenHandler *nextWindowOpenHandler = nullptr; if (_waitForWindowOpenEvalulation != 0) { - // - // - // - // - // - // - // - // + // + // + // + // + // + // + // + // unsigned long waitTime = 0; if (_windowOpenHandlers.size() == 2) { @@ -453,9 +522,9 @@ void ShutterControllerChannel::execute(CallContext &callContext) { _waitForWindowOpenEvalulation = 0; _windowOpenState = (((bool) KoSHC_CWindowOpenOpened1.value(DPT_OpenClose)) == !ParamSHC_CWindowOpenContactInvert2) ? WindowOpenState::WindowOpenStateOpen : WindowOpenState::WindowOpenStateClosed; - // - // - // + // + // + // switch (ParamSHC_CWindowTiltHandling) { case 0: @@ -510,6 +579,8 @@ void ShutterControllerChannel::execute(CallContext &callContext) if (_currentWindowOpenHandler != nextWindowOpenHandler) { sceneChanged = true; + bool startingWindowOpen = _currentWindowOpenHandler == nullptr && nextWindowOpenHandler != nullptr; + bool endingWindowOpen = _currentWindowOpenHandler != nullptr && nextWindowOpenHandler == nullptr; if (_currentWindowOpenHandler != nullptr && nextWindowOpenHandler != nullptr) logInfoP("Changing window open from %s to %s", _currentWindowOpenHandler->name(), nextWindowOpenHandler->name()); else @@ -519,15 +590,37 @@ void ShutterControllerChannel::execute(CallContext &callContext) else logInfoP("Stop window open"); } + if (startingWindowOpen) + { + _windowOpenRestoreActive = true; + _windowOpenRestorePosition = _positionController.position(); + _windowOpenRestoreSlat = _positionController.hasSlat() ? _positionController.slat() : 255; + if (callContext.diagnosticLog) + logInfoP("Window open restore: stored pos=%u slat=%u", + (unsigned int)_windowOpenRestorePosition, + (unsigned int)_windowOpenRestoreSlat); + } if (_currentWindowOpenHandler != nullptr) _currentWindowOpenHandler->stop(callContext, nextWindowOpenHandler, _positionController); _currentWindowOpenHandler = nextWindowOpenHandler; if (_currentWindowOpenHandler != nullptr) _currentWindowOpenHandler->start(callContext, _currentWindowOpenHandler, _positionController); + if (endingWindowOpen && _windowOpenRestoreActive) + { + if (callContext.diagnosticLog) + logInfoP("Window open restore: apply pos=%u slat=%u", + (unsigned int)_windowOpenRestorePosition, + (unsigned int)_windowOpenRestoreSlat); + if (_windowOpenRestorePosition != 255) + _positionController.setAutomaticPosition(_windowOpenRestorePosition); + if (_positionController.hasSlat() && _windowOpenRestoreSlat != 255) + _positionController.setAutomaticSlat(_windowOpenRestoreSlat); + _windowOpenRestoreActive = false; + } } callContext.isWindowOpenActive = _currentWindowOpenHandler != nullptr; - // State machine handling for mode activateion + // State machine handling for mode activation ModeBase *nextMode = nullptr; for (auto mode : _modes) { @@ -551,12 +644,12 @@ void ShutterControllerChannel::execute(CallContext &callContext) if (!shadingControlActive() && mode->isModeShading()) { if (callContext.diagnosticLog) - logInfoP("-> but ignored, because global shadow not alloewd"); + logInfoP("-> but ignored, because global shading not allowed"); } else nextMode = mode; // Do not break because allowed should be called for all modes - // because it is a replacment for the loop function + // because it is a replacement for the loop function } else { @@ -630,6 +723,30 @@ void ShutterControllerChannel::execute(CallContext &callContext) _currentMode->control(callContext, _positionController); _positionController.control(callContext); +#ifdef KoSHC_CShadingReadyUser + bool shadingModeLockActive = false; + if (ParamSHC_CShadingCount >= 1) + { + shadingModeLockActive = KoSHC_CShading1LockActive.value(DPT_Switch) || + KoSHC_CShading1BreakLockActive.value(DPT_Switch); + } +#ifdef KoSHC_CShading2LockActive + if (ParamSHC_CShadingCount >= 2) + { + shadingModeLockActive = shadingModeLockActive || + KoSHC_CShading2LockActive.value(DPT_Switch) || + KoSHC_CShading2BreakLockActive.value(DPT_Switch); + } +#endif + bool readinessUser = shadingControlActive() && + !_channelLockActive && + _currentWindowOpenHandler == nullptr && + _currentMode != _modeManual && + !shadingModeLockActive; + if (KoSHC_CShadingReadyUser.valueNoSendCompare(readinessUser, DPT_Switch)) + KoSHC_CShadingReadyUser.objectWritten(); +#endif + callContext.modeIdle = nullptr; callContext.modeManual = nullptr; callContext.modeCurrentActive = nullptr; @@ -688,23 +805,23 @@ void ShutterControllerChannel::anyShadingModeActive(bool active) KoSHC_CShadingActive.value(false, DPT_Switch); if (_currentMode != _modeManual) { - // - // - // - // + // + // + // + // switch (_positionController.hasSlat() ? ParamSHC_CAfterShadingJalousie : ParamSHC_CAfterShading) { case 1: - // Position vor Beschattungsstart + // Position before shading start _positionController.restoreLastManualPosition(); break; case 2: - // Fährt auf + // Move up _positionController.setAutomaticPositionAndStoreForRestore(0); // Handled as manual operation because the value should be stored _positionController.setAutomaticSlatAndStoreForRestore(0); // Handled as manual operation because the value should be stored break; case 3: - // Lamelle Waagrecht + // Slat horizontal _positionController.storeCurrentPositionForRestore(); _positionController.setAutomaticSlatAndStoreForRestore(50); // Handled as manual operation because the value should be stored break; diff --git a/src/ShutterControllerChannel.h b/src/ShutterControllerChannel.h index 207244c..ad54450 100755 --- a/src/ShutterControllerChannel.h +++ b/src/ShutterControllerChannel.h @@ -25,6 +25,9 @@ class ShutterControllerChannel : public OpenKNX::Channel std::string _name = std::string(); WindowOpenHandler* _currentWindowOpenHandler = nullptr; WindowOpenState _windowOpenState = WindowOpenStateClosed; + bool _windowOpenRestoreActive = false; + uint8_t _windowOpenRestorePosition = 255; + uint8_t _windowOpenRestoreSlat = 255; ModeManual* _modeManual = nullptr; ModeIdle* _modeIdle = nullptr; diff --git a/src/ShutterControllerModule.ModeShading.xml b/src/ShutterControllerModule.ModeShading.xml index 25b6c7e..dbf9ec1 100755 --- a/src/ShutterControllerModule.ModeShading.xml +++ b/src/ShutterControllerModule.ModeShading.xml @@ -14,7 +14,7 @@ - + @@ -275,6 +275,8 @@ + + @@ -293,6 +295,8 @@ + + @@ -321,6 +325,8 @@ + + @@ -374,12 +380,16 @@ - + + + - - - - + + + + + + @@ -395,12 +405,16 @@ - - - - - - + + + + + + + + + + diff --git a/src/ShutterControllerModule.ModeWindowOpen.xml b/src/ShutterControllerModule.ModeWindowOpen.xml index 1c082c1..9753f4c 100755 --- a/src/ShutterControllerModule.ModeWindowOpen.xml +++ b/src/ShutterControllerModule.ModeWindowOpen.xml @@ -10,7 +10,7 @@ - + diff --git a/src/ShutterControllerModule.cpp b/src/ShutterControllerModule.cpp index 3d71104..03dd2cc 100755 --- a/src/ShutterControllerModule.cpp +++ b/src/ShutterControllerModule.cpp @@ -1,5 +1,6 @@ #include "ShutterControllerModule.h" #include "ShutterControllerChannel.h" +#include ShutterControllerModule::ShutterControllerModule() @@ -57,7 +58,8 @@ void ShutterControllerModule::loop() _measurementTemperature.update(_callContext.currentMillis, _callContext.diagnosticLog); _measurementTemperatureForecast.update(_callContext.currentMillis, _callContext.diagnosticLog); - _measurementBrightness.update(_callContext.currentMillis, _callContext.diagnosticLog); + for (auto& sensor : _measurementBrightnessSensors) + sensor.update(_callContext.currentMillis, _callContext.diagnosticLog); _measurementUVIndex.update(_callContext.currentMillis, _callContext.diagnosticLog); _measurementRain.update(_callContext.currentMillis, _callContext.diagnosticLog); _measurementClouds.update(_callContext.currentMillis, _callContext.diagnosticLog); @@ -95,10 +97,12 @@ void ShutterControllerModule::loop() _callContext.azimuth = openknx.sun.azimuth(); _callContext.elevation = openknx.sun.elevation(); } + _brightnessMeasurement.update(_callContext.currentMillis, _callContext.diagnosticLog, _callContext.azimuth, _callContext.timeAndSunValid); _callContext.diagnosticLog = false; auto numberOfChannels = getNumberOfChannels(); for (uint8_t i = 0; i < numberOfChannels; i++) { + uint8_t _channelIndex = i; auto channel = (ShutterControllerChannel *)getChannel(i); if (channel != nullptr) { @@ -108,6 +112,7 @@ void ShutterControllerModule::loop() _callContext.localTime.hour == 0) channel->activateShading(); + _brightnessMeasurement.setUseAzimuth(ParamSHC_CBrightnessAzimuthEnabled != 0); channel->execute(_callContext); } } @@ -119,7 +124,7 @@ void ShutterControllerModule::loop() } _measurementTemperature.resetChanged(); _measurementTemperatureForecast.resetChanged(); - _measurementBrightness.resetChanged(); + _brightnessMeasurement.resetChanged(); _measurementUVIndex.resetChanged(); _measurementRain.resetChanged(); _measurementClouds.resetChanged(); @@ -165,7 +170,7 @@ bool ShutterControllerModule::processCommand(const std::string cmd, bool diagnos { if (moduleCommand.length() == 1) { - _measurementBrightness.logState(true); + _brightnessMeasurement.logState(true); return true; } logInfoP("Set brightness"); @@ -293,7 +298,8 @@ void ShutterControllerModule::processInputKo(GroupObject &ko) } _measurementTemperature.processIputKo(ko); _measurementTemperatureForecast.processIputKo(ko); - _measurementBrightness.processIputKo(ko); + for (auto& sensor : _measurementBrightnessSensors) + sensor.processIputKo(ko); _measurementUVIndex.processIputKo(ko); _measurementRain.processIputKo(ko); _measurementClouds.processIputKo(ko); @@ -329,14 +335,71 @@ void ShutterControllerModule::setup() (MeasurementWatchdogFallbackBehavior)ParamSHC_TempForecastFallbackMode); _callContext.measurementTemperatureForecast = &_measurementTemperatureForecast; - _measurementBrightness.init( + uint8_t brightnessInputs = ParamSHC_HasBrightnessInput ? (uint8_t)(1 + ParamSHC_BrightnessInputCount) : 0; + if (brightnessInputs > _measurementBrightnessSensors.size()) + brightnessInputs = (uint8_t)_measurementBrightnessSensors.size(); + for (size_t i = 0; i < _measurementBrightnessSensors.size(); i++) + { + GroupObject* brightnessKo = nullptr; + if (i < brightnessInputs) + { + switch (i) + { + case 0: + brightnessKo = &KoSHC_BrightnessInput; + break; + case 1: + brightnessKo = &KoSHC_BrightnessInput2; + break; + case 2: + brightnessKo = &KoSHC_BrightnessInput3; + break; + case 3: + brightnessKo = &KoSHC_BrightnessInput4; + break; + case 4: + brightnessKo = &KoSHC_BrightnessInput5; + break; + default: + break; + } + } + _measurementBrightnessSensors[i].init( + "Brightness", + brightnessKo, + ParamSHC_BrightnessWatchdog, + KNXValue((float)ParamSHC_BrightnessFallback * 1000.F), + DPT_Value_Lux, + (MeasurementWatchdogFallbackBehavior)ParamSHC_BrightnessFallbackMode); + } + + std::vector brightnessSensors; + brightnessSensors.reserve(_measurementBrightnessSensors.size()); + const uint16_t noAzimuthValue = 65535; + const uint16_t azimuths[] = { + ParamSHC_BrightnessAzimuth1, + ParamSHC_BrightnessAzimuth2, + ParamSHC_BrightnessAzimuth3, + ParamSHC_BrightnessAzimuth4, + ParamSHC_BrightnessAzimuth5 + }; + for (size_t i = 0; i < _measurementBrightnessSensors.size(); i++) + { + BrightnessSensor sensor; + sensor.watchdog = &_measurementBrightnessSensors[i]; + sensor.azimuth = azimuths[i] == noAzimuthValue ? 0 : azimuths[i]; + sensor.hasAzimuth = azimuths[i] != noAzimuthValue; + sensor.enabled = i < brightnessInputs; + brightnessSensors.push_back(sensor); + } + + _brightnessMeasurement.init( "Brightness", - ParamSHC_HasBrightnessInput ? &KoSHC_BrightnessInput : nullptr, - ParamSHC_BrightnessWatchdog, - KNXValue((float)ParamSHC_BrightnessFallback * 1000.F), - DPT_Value_Lux, - (MeasurementWatchdogFallbackBehavior)ParamSHC_BrightnessFallbackMode); - _callContext.measurementBrightness = &_measurementBrightness; + (MeasurementWatchdogFallbackBehavior)ParamSHC_BrightnessFallbackMode, + (float)ParamSHC_BrightnessFallback * 1000.F); + _brightnessMeasurement.setSensors(brightnessSensors); + _brightnessMeasurement.setAggregation((BrightnessAggregation)ParamSHC_BrightnessAggregation); + _callContext.measurementBrightness = &_brightnessMeasurement; _measurementUVIndex.init( "UV Index", diff --git a/src/ShutterControllerModule.h b/src/ShutterControllerModule.h index e338d0e..3be3590 100755 --- a/src/ShutterControllerModule.h +++ b/src/ShutterControllerModule.h @@ -2,7 +2,9 @@ #include "OpenKNX.h" #include "ChannelOwnerModule.h" #include "CallContext.h" +#include "BrightnessMeasurement.h" #include "MeasurementWatchdog.h" +#include class ShutterControllerModule : public ShutterControllerChannelOwnerModule { @@ -20,7 +22,9 @@ class ShutterControllerModule : public ShutterControllerChannelOwnerModule CallContext _callContext = CallContext(); MeasurementWatchdog _measurementTemperature = MeasurementWatchdog(); MeasurementWatchdog _measurementTemperatureForecast = MeasurementWatchdog(); - MeasurementWatchdog _measurementBrightness = MeasurementWatchdog(); + static constexpr uint8_t kBrightnessSensorCount = 5; + std::array _measurementBrightnessSensors; + BrightnessMeasurement _brightnessMeasurement; MeasurementWatchdog _measurementUVIndex = MeasurementWatchdog(); MeasurementWatchdog _measurementRain = MeasurementWatchdog(); MeasurementWatchdog _measurementClouds = MeasurementWatchdog(); diff --git a/src/ShutterControllerModule.share.xml b/src/ShutterControllerModule.share.xml index 6bd62a7..fa47099 100755 --- a/src/ShutterControllerModule.share.xml +++ b/src/ShutterControllerModule.share.xml @@ -40,6 +40,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -369,7 +472,7 @@ - + @@ -457,6 +560,23 @@ + + + + + + + + + + + + + + + + + @@ -537,6 +657,20 @@ + + + + + + + + + + + + + + @@ -557,6 +691,14 @@ + + + + + + + + @@ -577,6 +719,14 @@ + + + + + + + + @@ -700,6 +850,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ShutterControllerModule.templ.xml b/src/ShutterControllerModule.templ.xml index 8fe63d8..a549982 100755 --- a/src/ShutterControllerModule.templ.xml +++ b/src/ShutterControllerModule.templ.xml @@ -10,7 +10,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -76,7 +76,8 @@ - + + @@ -89,7 +90,10 @@ - + + + + @@ -199,7 +203,7 @@ - + @@ -233,6 +237,9 @@ + + + @@ -294,6 +301,8 @@ + + @@ -414,16 +423,19 @@ - + + + + - + - + @@ -493,6 +505,8 @@ + + @@ -616,6 +630,18 @@ + + + + + + + + + + + + @@ -630,9 +656,13 @@ + + + + diff --git a/src/WindowOpenHandler.cpp b/src/WindowOpenHandler.cpp index fa41541..65ff058 100644 --- a/src/WindowOpenHandler.cpp +++ b/src/WindowOpenHandler.cpp @@ -171,9 +171,9 @@ void WindowOpenHandler::start(const CallContext &callContext, const WindowOpenHa { auto positionControl = getParamterOpenPositionControl(); - // - // - // + // + // + // if (positionControl > 0) { auto position = getParamterOpenPosition(); @@ -185,9 +185,9 @@ void WindowOpenHandler::start(const CallContext &callContext, const WindowOpenHa if (positionController.hasSlat()) { auto slatPositionControl = getParamterOpenSlatPositionControl(); - // - // - // + // + // + // if (slatPositionControl > 0) { auto slatPosition = getParamterOpenSlatPosition(); diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..00984c0 --- /dev/null +++ b/test/README.md @@ -0,0 +1,19 @@ +# Tests (OFM-ShutterControllerModule) + +## Python (Logik-Simulation) + +Dieser Test laeuft ohne Hardware und simuliert die Helligkeitslogik. + +```bash +python lib/OFM-ShutterControllerModule/test/brightness_logic_test.py +``` + +## C++ (PlatformIO Unity) + +Diese Tests werden ueber PlatformIO ausgefuehrt. + +```bash +pio test -e develop_RP2040 -f test_brightness_logic +``` + +Hinweis: Die Tests laufen im PlatformIO-Umfeld und nutzen Unity. diff --git a/test/brightness_logic_test.py b/test/brightness_logic_test.py new file mode 100644 index 0000000..1cb89aa --- /dev/null +++ b/test/brightness_logic_test.py @@ -0,0 +1,149 @@ +import math +import unittest + + +def normalize_azimuth(azimuth): + while azimuth < 0.0: + azimuth += 360.0 + while azimuth >= 360.0: + azimuth -= 360.0 + return azimuth + + +def aggregate_states(sensors, only_unassigned): + values = [] + for sensor in sensors: + if not sensor["valid"]: + continue + if only_unassigned and sensor["has_azimuth"]: + continue + values.append(sensor["value"]) + if not values: + return {"count": 0, "mean": None, "max": None} + return { + "count": len(values), + "mean": sum(values) / float(len(values)), + "max": max(values), + } + + +def build_azimuth_state(sensors, sun_azimuth, sun_valid, aggregate_value): + values = [] + for sensor in sensors: + if not sensor["valid"]: + continue + if not sensor["has_azimuth"]: + continue + values.append((float(sensor["azimuth"]), float(sensor["value"]))) + + if not sun_valid: + values = [] + + if not values: + return aggregate_value + + values.sort(key=lambda x: x[0]) + merged = [] + for az, val in values: + if merged and abs(merged[-1][0] - az) < 0.001: + merged[-1] = (az, (merged[-1][1] + val) / 2.0) + else: + merged.append((az, val)) + + if len(merged) == 1: + return merged[0][1] + + target = normalize_azimuth(sun_azimuth) + result = merged[0][1] + for i in range(len(merged)): + a0, v0 = merged[i] + a1, v1 = merged[(i + 1) % len(merged)] + segment_start = a0 + segment_end = a1 + if i == len(merged) - 1: + segment_end += 360.0 + + target_wrapped = target + if target_wrapped < segment_start: + target_wrapped += 360.0 + + if segment_start <= target_wrapped <= segment_end: + ratio = (target_wrapped - segment_start) / (segment_end - segment_start) if segment_end > segment_start else 0.0 + result = v0 + (v1 - v0) * ratio + break + + return result + + +def evaluate_brightness(sensors, aggregation, use_azimuth, prefer_unassigned, force_max, sun_azimuth, sun_valid): + states_all = aggregate_states(sensors, False) + states_unassigned = aggregate_states(sensors, True) + use_unassigned = prefer_unassigned and states_unassigned["count"] > 0 + + mean_val = states_unassigned["mean"] if use_unassigned else states_all["mean"] + max_val = states_unassigned["max"] if use_unassigned else states_all["max"] + + aggregate_value = max_val if (force_max or aggregation == "max") else mean_val + + if aggregate_value is None: + aggregate_value = 0.0 + + if use_azimuth: + return build_azimuth_state(sensors, sun_azimuth, sun_valid, aggregate_value) + + return aggregate_value + + +class BrightnessLogicTests(unittest.TestCase): + def test_azimuth_interpolation(self): + sensors = [ + {"value": 100.0, "has_azimuth": True, "azimuth": 90, "valid": True}, + {"value": 200.0, "has_azimuth": True, "azimuth": 180, "valid": True}, + ] + value = evaluate_brightness( + sensors, + aggregation="mean", + use_azimuth=True, + prefer_unassigned=False, + force_max=False, + sun_azimuth=135.0, + sun_valid=True, + ) + self.assertTrue(math.isclose(value, 150.0, rel_tol=0.0, abs_tol=0.01)) + + def test_roof_prefers_unassigned(self): + sensors = [ + {"value": 100.0, "has_azimuth": True, "azimuth": 90, "valid": True}, + {"value": 200.0, "has_azimuth": True, "azimuth": 180, "valid": True}, + {"value": 50.0, "has_azimuth": False, "azimuth": 0, "valid": True}, + ] + value = evaluate_brightness( + sensors, + aggregation="mean", + use_azimuth=False, + prefer_unassigned=True, + force_max=True, + sun_azimuth=0.0, + sun_valid=False, + ) + self.assertTrue(math.isclose(value, 50.0, rel_tol=0.0, abs_tol=0.01)) + + def test_roof_fallback_all_max(self): + sensors = [ + {"value": 100.0, "has_azimuth": True, "azimuth": 90, "valid": True}, + {"value": 200.0, "has_azimuth": True, "azimuth": 180, "valid": True}, + ] + value = evaluate_brightness( + sensors, + aggregation="mean", + use_azimuth=False, + prefer_unassigned=True, + force_max=True, + sun_azimuth=0.0, + sun_valid=False, + ) + self.assertTrue(math.isclose(value, 200.0, rel_tol=0.0, abs_tol=0.01)) + + +if __name__ == "__main__": + unittest.main() From 7bd5322e6b84b2318822918adbebb9112e5a0794 Mon Sep 17 00:00:00 2001 From: estebanri87 <42171761+estebanri87@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:58:11 +0100 Subject: [PATCH 2/2] Update toDo + Release Notes --- ReleaseNotes.md | 7 ++ ToDos.md | 11 +-- doc/Testplan-ShutterController.md | 131 ------------------------------ 3 files changed, 9 insertions(+), 140 deletions(-) delete mode 100644 doc/Testplan-ShutterController.md diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 0e55044..8a53f4e 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,10 @@ +v 0.5.0 +- Feature: Neues KO "Status Beschattung Bereit" je Kanal +- Feature: Dachflaeche bevorzugt unzugeordnete Helligkeitssensoren (z.B. Dachsensor) +- Feature: Wiederherstellung der vorherigen Position nach Fenster offen/gekippt +- Fix: Azimut-/Helligkeit-UI klarer (Helligkeit Sensor 1..5, "Keine Himmelsrichtung (Azimut-Auswertung aus)") +- Fix: Schreibfehler und Himmelsrichtungsbezeichnungen korrigiert +- Doc: Help-Context und Applikationsbeschreibung aktualisiert v 0.4.0 - Feature: Invertieren der Fensterkontakt KO - Feature: Auswahl verhalten der Fensterkontakte diff --git a/ToDos.md b/ToDos.md index 25aed1f..2ec68fb 100644 --- a/ToDos.md +++ b/ToDos.md @@ -1,14 +1,7 @@ To Do's +- Lamellenstellung Beschattung Grundposition wählbar mit maximaler Abweichung von der Grundeinstellung um nicht den Raum komplett zu verdunkeln. +- Optionale Status KO´s & Sperren ausblendbar um die Anzahl der sichtbaren KO´s zu reduzieren (falls sie nicht benötigt werden). -- Gekippt heißt nur, das Offen als gekippt interpertiert werden soll: - -Offen = 1 bedeutet nicht zu, ist also bei Gekippt = 1 auch 1 -Offen = 0 bedeutet zu, ganz egal was Gekippt anzeigt (Gekippt ist hier immer 1, technisch bedingt) -Das Fenster ist gekippt, wenn Offen = 1 und Gekippt = 1 -Das Fenster ist offen, wenn Offen = 1 und Gekkippt = 0. -Verbinde ich das direkt mit Deinen Eingängen, hab ich quasi bei geschlossenem Fenster immer Gekippt und bei gekipptem oder offenen Fenster immer Offen. - -- Getrennte Einstellungen für Fenster offen bei Tag und Nach Long Term To Do'S - Szenen diff --git a/doc/Testplan-ShutterController.md b/doc/Testplan-ShutterController.md deleted file mode 100644 index 348d37d..0000000 --- a/doc/Testplan-ShutterController.md +++ /dev/null @@ -1,131 +0,0 @@ -# Testplan ShutterController - -Dieser Testplan deckt Logik-, Integrations- und HIL/Manuell-Tests fuer die Beschattungslogik ab. -Ziel ist, die Auswertung von Helligkeit/Azimut, Prioritaeten der Modi, Fallback-Verhalten -sowie ETS/KNX-Integration sauber nachzuweisen. - -## 1) Automatisierte Logiktests (Unit) - -### 1.1 Helligkeit / Azimut / Dachflaeche -- TC-BRI-001: Azimut-Auswertung aktiv, 3 Sensoren mit Azimut (O/S/W), gueltiger Sonnenazimut. - - Erwartung: Azimut-Interpolation verwendet nur Sensoren mit Azimut. -- TC-BRI-002: Dachflaeche, 4 Sensoren mit Azimut + 1 Sensor ohne Azimut. - - Erwartung: Dachflaeche bevorzugt Sensor(en) ohne Azimut, nimmt Max der unzugeordneten. -- TC-BRI-003: Dachflaeche, nur Sensoren mit Azimut. - - Erwartung: Max ueber alle Sensoren. -- TC-BRI-004: Keine Himmelsrichtung (Azimut-Auswertung aus), Aggregation=Mean. - - Erwartung: Mittelwert aller gueltigen Sensoren. -- TC-BRI-005: Keine Himmelsrichtung (Azimut-Auswertung aus), Aggregation=Max. - - Erwartung: Max aller gueltigen Sensoren. -- TC-BRI-006: Sonnenazimut ungueltig. - - Erwartung: Azimut-Auswertung faellt auf Aggregatwert. -- TC-BRI-007: Sensor-Watchdog ignoreValue/waitForValue. - - Erwartung: Nicht-gueltige Sensoren werden nicht in Aggregat/Azimut genutzt. -- TC-BRI-008: Fallback-Mode Provide/Ignore. - - Erwartung: Fallback-Wert oder Ignorieren gemaess Einstellung. - -### 1.2 Modus-Prioritaeten -- TC-MOD-001: Fenster offen aktiv. - - Erwartung: Fenster-Modus hat hoehere Prioritaet als Handbetrieb/Nacht/Beschattung. -- TC-MOD-002: Handbetrieb aktiv, Fenster nicht aktiv. - - Erwartung: Handbetrieb vor Nacht/Beschattung. -- TC-MOD-003: Nachtmodus aktiv, keine Sperren. - - Erwartung: Nachtmodus vor Beschattung. -- TC-MOD-004: Mehrere Beschattungsmodi erlaubt. - - Erwartung: Hoechste erlaubte Nummer wird aktiv. - -### 1.3 Grenzen, Hysterese, Wartezeiten -- TC-LIM-001: Azimut/Elevation innerhalb Grenzen. - - Erwartung: Beschattung zulassbar. -- TC-LIM-002: Azimut/Elevation ausserhalb Grenzen. - - Erwartung: Beschattung nicht zulassbar. -- TC-LIM-003: Helligkeit Hysterese. - - Erwartung: Kein Flattern bei kleinen Schwankungen. -- TC-LIM-004: Beschattungsstart/Beschattungsende Wartezeiten. - - Erwartung: Aktivierung/Deaktivierung erst nach Ablauf. - -### 1.4 Fenster-Offen/Geoeffnet/GeKippt -- TC-WIN-001: Fensterkontakt 1 aktiv, 2 inaktiv. - - Erwartung: Fenster offen Modus. -- TC-WIN-002: Fensterkontakt 2 aktiv, je nach Konfiguration. - - Erwartung: Fenster gekippt Modus. -- TC-WIN-003: Restore vorheriger Position nach Fenster-Modus Ende. - - Erwartung: Position/Slat werden wiederhergestellt. - -### 1.5 Szenarien und Ergebnisse - -| TC | Szenario (Kurzform) | Automatisiert | Ergebnis | -| --- | --- | --- | --- | -| TC-BRI-001 | 2 Azimut-Sensoren (90/180), Sonnenazimut 135 -> Interpolation | Python | PASS | -| TC-BRI-002 | Dachflaeche, 2 Azimut + 1 ohne Azimut -> Max(ohne Azimut) | Python | PASS | -| TC-BRI-003 | Dachflaeche, nur Azimut-Sensoren -> Max(alle) | Python | PASS | -| TC-BRI-004 | Azimut-Auswertung aus, Aggregation=Mean | Nicht automatisiert | NICHT GETESTET | -| TC-BRI-005 | Azimut-Auswertung aus, Aggregation=Max | Nicht automatisiert | NICHT GETESTET | -| TC-BRI-006 | Sonnenazimut ungueltig -> Aggregatwert | Nicht automatisiert | NICHT GETESTET | -| TC-BRI-007 | Watchdog ignore/wait -> Sensoren ausgeschlossen | Nicht automatisiert | NICHT GETESTET | -| TC-BRI-008 | Fallback Provide/Ignore | Nicht automatisiert | NICHT GETESTET | -| TC-MOD-001 | Fenster offen aktiv -> Prioritaet hoch | Nicht automatisiert | NICHT GETESTET | -| TC-MOD-002 | Handbetrieb aktiv -> Prioritaet vor Nacht/Beschattung | Nicht automatisiert | NICHT GETESTET | -| TC-MOD-003 | Nachtmodus aktiv -> vor Beschattung | Nicht automatisiert | NICHT GETESTET | -| TC-MOD-004 | Mehrere Beschattungsmodi -> hoechste Nummer | Nicht automatisiert | NICHT GETESTET | -| TC-LIM-001 | Azimut/Elevation innerhalb Grenzen | Nicht automatisiert | NICHT GETESTET | -| TC-LIM-002 | Azimut/Elevation ausserhalb Grenzen | Nicht automatisiert | NICHT GETESTET | -| TC-LIM-003 | Helligkeit Hysterese | Nicht automatisiert | NICHT GETESTET | -| TC-LIM-004 | Wartezeiten Start/Ende | Nicht automatisiert | NICHT GETESTET | -| TC-WIN-001 | Kontakt 1 aktiv, Kontakt 2 inaktiv | Nicht automatisiert | NICHT GETESTET | -| TC-WIN-002 | Kontakt 2 aktiv (je nach Konfig) | Nicht automatisiert | NICHT GETESTET | -| TC-WIN-003 | Restore Position/Slat nach Fenster-Modus | Nicht automatisiert | NICHT GETESTET | - -Hinweise: -- PlatformIO C++ Testlauf konnte nicht gestartet werden ("pio" nicht verfuegbar, Python-Modul "platformio" nicht installiert). - -## 2) Simulation/Integration (teil-automatisiert) - -### 2.1 KO-Input-Simulation -- TC-SIM-001: Simuliere KO Eingaben fuer Helligkeit/Temp/Wolken/Regen. - - Erwartung: Beschattung erlaubt/nicht erlaubt gem. Grenzen. -- TC-SIM-002: Simuliere Fensterkontakte + Handbetrieb. - - Erwartung: Prioritaet Fenster > Hand > Nacht > Beschattung. - -### 2.2 Szenarien (End-to-End Logik) -- TC-SCEN-001: 4 Fassadensensoren + 1 Dachsensor. - - Erwartung: Fassadenkanaele nutzen Azimut, Dachflaeche nutzt Dachsensor. -- TC-SCEN-002: Nur Dachsensor, Dachflaeche. - - Erwartung: Dachsensor bestimmt Helligkeit. -- TC-SCEN-003: Nur Fassadensensoren, Dachflaeche. - - Erwartung: Max ueber alle Sensoren. -- TC-SCEN-004: Azimut-Auswertung aus. - - Erwartung: Aggregation ueber alle Sensoren. - -## 3) HIL / Manuell (ETS + Hardware) - -### 3.1 ETS/KNXprod -- TC-ETS-001: Import .knxprod, Seiten/Labels pruefen. - - Erwartung: "Keine Himmelsrichtung (Azimut-Auswertung aus)" sichtbar. -- TC-ETS-002: Help-Context Links. - - Erwartung: Alle Parameter zeigen passende Hilfe. - -### 3.2 Bus-Telegramme -- TC-BUS-001: Echte Gruppenadressen senden (Helligkeit/Temp/Lock). - - Erwartung: Beschattung reagiert gemaess Logik. -- TC-BUS-002: Fensteroeffnen/Kippen. - - Erwartung: Moduswechsel + Restore. - -### 3.3 Hardwareverhalten -- TC-HW-001: Positionsfahrt und Lamellenstellung. - - Erwartung: Werte korrekt angefahren. -- TC-HW-002: Timing/Watchdog. - - Erwartung: Fallback/Ignore Verhalten. - -## 4) Automatisierungs-Optionen - -- Unit-Tests als C++ Tests (z.B. in OFM-ShutterControllerModule/test). -- Python-Tests fuer Logik (fuer reine Berechnungen/Mocks). -- Simulation ueber Ko-Eingaben (CI-freundlich, keine Hardware noetig). - -## 5) Abnahmekriterien - -- Alle Unit-Tests gruen. -- Kritische Szenarien (Dachflaeche, Azimut-Auswertung aus) nachweislich korrekt. -- ETS-UI/Help-Context konsistent zur Doku. -- HIL-Tests erfolgreich fuer reale Telegramme und Fahrverhalten.