diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0877619 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +test/ +Plan-Lamellennachführung-Schattenkantennachführung.md +ToDos.md diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 3cba9b7..ffcd72f 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,7 +1,47 @@ +v 0.6.4 +- Feature: Neue Einstellung „Betriebsart Lamellennachführung" für die Modi 5 und 6 (Geo. Positions- und Lamellennachführung). Vier Optionen: „Direktlicht abschneiden (Cut-Off)" (bisheriges Verhalten: geometrische Cut-Off-Formel), „Blendschutz (Retro-Reflexion)" (Retro-Reflexion: θ = Profilwinkel), „Adaptiv (Daylight)" (θ = min(θ_CutOff, θ_Retro) – maximale Tageslichtnutzung bei gleichzeitigem Blendschutz) und „Über Tabelle" (6 konfigurierbare Stützpunkte Höhenwinkel → Lamellenposition % mit linearer Interpolation, Standardwerte gemäß Warema-Tabelle). +- Feature: Neuer Parameter „Beschattungsstart" – legt fest, ob die Beschattung sofort (mit Schutzposition) oder erst bei direktem Sonnenlicht (Profilwinkel > 0°) startet. +- Change: Helligkeitsmessung über Azimut auf cosinus-gewichtete Interpolation umgestellt – alle freigegebenen Helligkeitssensoren werden gleichzeitig nach ihrer Winkeldifferenz zur Sonne gewichtet (statt nur die zwei nächstliegenden linear zu interpolieren). Liefert stabilere Werte bei mehreren Sensoren. +- Change: „Status Beschattung Bereit (Benutzer)" wird jetzt auch ohne aktiven Beschattungsmodus korrekt als bereit gemeldet, sobald alle anderen Bedingungen erfüllt sind (vorher fälschlich nur bereit, wenn bereits ein Beschattungsmodus aktiv war). +- Fix: Lamellenwinkel-Skalierung bei geometrischer Berechnung korrigiert – der Nenner war fälschlicherweise „Lamellenwinkel vollständig geschlossen" (Winkel von der Vertikalen), korrekt ist (90° − dieser Wert) (Winkel von der Waagrechten bis vollständig geschlossen). Auswirkung: Lamellen wurden bisher fast immer auf 100% (oder den Clip-Maximalwert) gesetzt, jetzt werden physikalisch korrekte Zwischenstellungen berechnet. +- Fix: KO „Status Beschattung Bereit (Benutzer)" wird beim Geräteneustart explizit mit „false" initialisiert (vorher uninitialisiert auf dem Bus sichtbar). +- Fix: Speicherüberlappung im Beschattungsmodus-Block behoben – die Parameter „Beschattungsstart" (RequireFacadeHit) und der Tabellen-Höhenwinkel (SlatTableMinElevation) lagen fälschlicherweise innerhalb der 4-Byte-Float-Parameter „Minimaler UV-Index" bzw. „Minimale Raumtemperatur" und überschrieben sich gegenseitig. Beide wurden auf eigene Bytes am Blockende verschoben. +- Breaking: Parameterlayout des Beschattungsmodus-Blocks vergrößert (increment 52 → 68 Bytes / 416 → 544 Bit). Betrifft Kanäle mit zwei konfigurierten Beschattungsmodi. Bestehende ETS-Projekte müssen neu parametriert werden. +- Breaking: Parameterwert für „Über Tabelle" verschoben (Value 2 → 3). Bestehende ETS-Projekte mit „Über Tabelle"-Konfiguration müssen neu parametriert werden. + +v 0.6.3 +- Redesign: Beschattungsmodi Jalousie komplett neu strukturiert – aufbauende Modi-Reihe: Nein → Standard → Lamellenführung (Min/Max) → Geo. Positionsnachführung → Geo. Positionsnachführung (Min/Max) → Geo. Positions- und Lamellennachführung → Geo. Positions- und Lamellennachführung (Min/Max). +- Feature: Neuer Beschattungsmodus 6 „Geo. Positions- und Lamellennachführung (Min/Max)" – entspricht Modus 5 mit konfigurierbarer Min./Max.-Begrenzung für Position und Lamellenstellung. Wenn Min. Position > Max. Position konfiguriert ist, wird die Positionsbegrenzung ignoriert. Die Lamellenstellungsbegrenzung verwendet automatisch min/max der beiden Werte (Reihenfolge egal). +- Feature: Neuer Rollo-Modus „Geo. Positionsnachführung (Min/Max)" (Wert 2) – entspricht Modus 1 mit Min./Max.-Positionsbegrenzung. +- Breaking: Parameterlayout des Beschattungsmodus-Blocks vergrößert (increment 50 → 52). Betrifft nur Kanäle mit zwei oder mehr konfigurierten Beschattungsmodi. Bestehende ETS-Projekte mit nur einem Beschattungsmodus pro Kanal sind nicht betroffen. +- Breaking: Modus 3 (war „Lamellennachführung Experte") wird jetzt „Geo. Positionsnachführung" (Eindringtiefe + feste Lamellenstellung). Der alte Experte-Modus entfällt. Bestehende ETS-Projekte mit Modus 3 müssen neu konfiguriert werden. +- Rename: Beschattungsmodus 4 umbenannt: „Schattenkantennachführung" → „Geo. Positionsnachführung (Min/Max)". Beschattungsmodus 5 umbenannt: „Schattenkanten- und Lamellenführung" → „Geo. Positions- und Lamellennachführung". Die Parameterwerte (4, 5) sind unverändert. +- Rename: ETS-Parameter „Lamellenstellenung an Sonnenstand anpassen" (Tippfehler) → „Positions- und Lamellennachführung". +- Rename: Rollo-Parameter „Schattenkantennachführung" → „Geo. Positionsnachführung" (Wert 1 unverändert). +- Fix: Kritischer Kippwinkel (theta_krit) wurde fälschlicherweise als 90°−atan2(...) berechnet; korrekt ist direkt atan2(...) (Winkelkonvention zur Senkrechten). +- Fix: Bei Dachflächen-Orientierung wurde sin_alpha ohne Betrag berechnet, was bei negativen Neigungswinkeln zu falschen Positionen führte (jetzt fabsf). +- Fix: OffsetSlatPosition wurde in Modus 5 nicht auf die berechnete Lamellenstellung angewendet. +- Fix: Hysterese (MinChangeForSlatAdaption) fehlte in Modus 2 (Standard-Lamellennachführung) vor dem Setzen der Lamellenstellung. +- Change: Wertebereich „Max. Eindringtiefe" angepasst: 1–200 → 10–255 cm. + +v 0.6.2 +- Feature: Neue Beschattungsmodi 3 „Lamellennachführung Experte", 4 „Schattenkantennachführung", 5 „Schattenkanten- und Lamellennachführung" +- Feature: Schattenkantennachführung für Rollo (neuer Parameter „Positionsnachführung" im Rollo-Kanal) +- Feature: Neuer Kanal-Parameter „Fassadenneigung/Fensterneigung" für geometrische Berechnung +- Breaking/Rename: Beschattungsmodus-Lamellensteuerung Wert 2 umbenannt: „Benutzerdefiniert" → „Lamellennachführung Min/Max". Der Parameterwert (2) ist unverändert. Bestehende ETS-Projekte laufen ohne Migration weiter; lediglich der angezeigte Text im ETS-Dropdown ändert sich nach einem Update der knxprod. + +v 0.6.1 +- Feature: Positionsprüfung im Status „Beschattung Bereit (Benutzer)" – Beschattungsbereitschaft wird nur signalisiert, wenn die aktuelle Zielposition ≤ dem konfigurierten Grenzwert „Nur wenn Position kleiner als" ist +- Feature: KO-Freigabe-Parameter je Kanal – alle optionalen KOs können nun einzeln in der ETS ein-/ausgeblendet werden (Sperren Kanal, Status aktiver Modus, Aktorrückmeldung, Beschattung ein/aus, Beschattung aktiv, Beschattungsbereitschaft, Handbetrieb Schalten, Handbetrieb Position, Fenster offen/gekippt Status und Sperren) +- Feature: KO-Freigabe-Parameter je Beschattungsmodus – Sperren, Status Aktiv und Status Bereitschaft je Beschattungsmodus separat freigebbar +- Feature: KO-Freigabe-Parameter je Fensterkontakt – Status und Sperren für Fenster offen / Fenster gekippt separat freigebbar +- Rename: KO-Freigabe-Parameter für Beschattungsmodus konsistent benannt (PPP+53 = Sperren freigeben, PPP+54 = Bereitschaft freigeben, PPP+55 = Status Aktiv freigeben) + v 0.6.0 - Refactor: "Helligkeit" (bool) + "Weitere Helligkeitssensoren" (count) zusammengefasst zu "Helligkeitssensoren" (Enum: Nein/1-5 Sensoren) - Rename: "Helligkeit Sensor 1..5" -> "Ausrichtung Sensor 1..5" - Breaking: Offset 19 Semantik geaendert (SHC_VerifyVersion 0.5 -> 0.6) + v 0.5.0 - Feature: Neues KO "Status Beschattung Bereit" je Kanal - Feature: Dachflaeche bevorzugt unzugeordnete Helligkeitssensoren (z.B. Dachsensor) @@ -9,10 +49,13 @@ v 0.5.0 - 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.2 - Fix: Stopp von Rolladen bei manueller Bedienung durch Jalousiensteuerung + v 0.4.1 - Feature: Bessere Bennenung der Gruppenobjekte + v 0.4.0 - Feature: Invertieren der Fensterkontakt KO - Feature: Auswahl verhalten der Fensterkontakte @@ -21,8 +64,10 @@ v 0.4.0 - Fix: die KOs für offen und kipp waren vertauscht - Fix: Wenn man mehrmals zwischen kipp und offen wechselt, fährt der Rollladen beim Schließen nicht in die Position vor dem öffnen, sondern in eine der kipp- oder offen-Positionen. - Fix: Gruppe offen / gekippt in der ETS Baumansicht vertauscht + v 0.3.0 - Fix: Handsteuerungslogik + v 0.2.0 - Bugfix: Lammellen-KO wird bei Gerätetpye 'Rollo' angezeigt - Bugfix: Position anfahren bei Fenster offen/gekippt diff --git a/ToDos.md b/ToDos.md index 2ec68fb..ffc1773 100644 --- a/ToDos.md +++ b/ToDos.md @@ -1,11 +1,8 @@ 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). +- Neigung der Fassade & Schattenkantennachführung & Erweiterung Lamellennachführung + Long Term To Do'S - Szenen -- Hysterese bei Helligkeit -- Neigung der Fassade -- Schattenkantennachführung - +- Hysterese bei Helligkeit \ No newline at end of file diff --git a/doc/Applikationsbeschreibung-ShutterController.md b/doc/Applikationsbeschreibung-ShutterController.md index bc8e1c2..89d6d6c 100644 --- a/doc/Applikationsbeschreibung-ShutterController.md +++ b/doc/Applikationsbeschreibung-ShutterController.md @@ -225,8 +225,8 @@ Beispiele (vereinfachte Sicht): | 3x Sensor mit Azimut (O/S/W) | Dachflaeche | Max(alle 3 Sensoren) — Fallback, da kein unzugeordneter Sensor | | 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Dachflaeche | Max(nur der Sensor ohne Azimut) | | 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Sued | Azimut-Interpolation nur mit den 4 Azimut-Sensoren | -| 2x Sensor ohne Azimut | Keine Himmelsrichtungsauswertung | Aggregation ueber alle Sensoren ohne Azimut | -| 1x Sensor mit Azimut | Keine Himmelsrichtungsauswertung | Aggregation ueber alle gueltigen Sensoren | +| 2x Sensor ohne Azimut | Keine Himmelsrichtungsauswertung | Aggregation über alle Sensoren ohne Azimut | +| 1x Sensor mit Azimut | Keine Himmelsrichtungsauswertung | Aggregation über alle gueltigen Sensoren | ##### Ausrichtung Sensor 1..5 @@ -800,6 +800,17 @@ Diese Angabe wird verwendet, um den passenden Helligkeitssensor fuer die Beschat - **Dachflaeche**: Bevorzugt Sensoren ohne Azimut-Zuordnung (z.B. Dachsensor). Die eingestellte Aggregation wird ignoriert — es gilt immer der Maximalwert. Falls keine Sensoren ohne Azimut-Zuordnung vorhanden sind, greift ein automatischer Fallback: alle Sensoren werden mit Max-Aggregation ausgewertet. - **Keine Himmelsrichtungsauswertung**: Azimut-basierte Sensorauswahl ist deaktiviert. Alle gueltigen Sensoren werden mit der eingestellten Aggregation (Mittelwert/Maximum) zusammengefasst. + +#### Fassadenneigung/Fensterneigung + +Die Neigung der Fassade oder eines Dachfenster in Grad, gemessen gegenüber der Senkrechten (0° = senkrechte Wand). + +- **0°**: Senkrechte Fassade/Senkrechtes Fenster (Standardfall) +- **Positiver Wert**: Fassade/Fenster neigt sich nach außen (z.B. überhängende Dachkante) +- **Negativer Wert**: Fassade/Fenster neigt sich nach innen (z.B. nach innen geneigte Wand) + +Dieser Wert wird für die Berechnung des Profilwinkels bei der **Geometrischen Positionsnachführung** und der **Geo. Positions- und Lamellennachführung** verwendet. Bei senkrechten Fenstern kann der Standardwert 0° belassen werden. + #### Nur starten wenn aktuelle Position kleiner gleich @@ -813,20 +824,97 @@ Eine Einstellung von 0% startet die Beschattung nur, wenn die Jalousie zuvor vol Position die bei Beschattungsstart angefahren wird. +Bei den Modi **Geometrische Positionsnachführung**, **Geometrische Positions- und Lamellennachführung** und **Geo. Positions- und Lamellennachführung (Min/Max)** wird die Position normalerweise dynamisch anhand des Sonnenstands berechnet. Die Beschattungsposition wirkt in diesen Modi nur als **Fallback**: Wenn die Sonne so flach steht, dass die berechnete Schattenkante oberhalb der Fensterhöhe liegt (d.h. der Behang muss vollständig abgesenkt sein), wird die hier konfigurierte Position als Zielwert verwendet. + +**Schutzposition:** Bei Beschattungsstart kann die Sonne zwar im konfigurierten Azimut- und Helligkeitsfenster liegen, aber geometrisch noch nicht auf die Fassade treffen (z.B. Westfenster, Sonne kommt noch aus Südosten). In diesem Fall fährt der Behang sofort auf die Beschattungsposition als Schutz gegen indirektes Licht und Reflexionen. Sobald die Sonne die Fassade trifft, übernimmt die geometrische Nachführung. + +Hinweis: Die Beschattungsposition wird beim Start **nicht** durch Min./Max.-Begrenzungen eingeschränkt. Soll dieses Verhalten vermieden werden, kann unter **Beschattungsstart** die Option "Nur bei direktem Sonnenlicht" gewählt werden. + + +#### Beschattungsstart + +Legt fest, unter welcher Bedingung die Beschattung gestartet wird, wenn alle Mess- und Sonnenwert-Freigaben erfüllt sind. Diese Einstellung ist nur bei aktivem Geo-Tracking sichtbar (Jalousie: Modi 3–6; Rollo: Modi 1–2). + +**Sofort (mit Schutzposition)** (Standard): +Die Beschattung startet sofort, sobald Azimut, Elevation und Helligkeitswerte die konfigurierten Grenzen erfüllen. Es ist dabei möglich, dass die Sonne geometrisch noch nicht direkt auf die Fassade trifft (z.B. beim Westfenster kommt die Sonne zunächst noch aus Südosten). In diesem Fall fährt der Behang unmittelbar auf die konfigurierte **Beschattungsposition** als Schutz gegen indirektes Licht und Reflexionen. Sobald die Sonne die Fassade trifft (Profilwinkel > 0°), übernimmt die geometrische Nachführung die Positionsberechnung. + +**Nur bei direktem Sonnenlicht**: +Die Beschattung startet erst, wenn die Sonne sowohl alle Mess- und Sonnenwert-Bedingungen erfüllt als auch geometrisch auf die Fassade trifft (Profilwinkel > 0°). Solange die Sonne noch nicht auf die Fassade trifft, bleibt der Behang geöffnet – die Beschattungsposition wird nicht angefahren. + +**Beispiel Westfenster** (Azimut 120°–290°): Die Sonne trifft eine Westfassade erst ab ca. 180° Azimut. +- *Sofort*: Behang fährt ab 120° auf Beschattungsposition, geometrische Nachführung ab ~180°. +- *Nur bei direktem Sonnenlicht*: Behang bleibt bis ~180° vollständig geöffnet. + -#### Lamellenstellenung an Sonnenstand anpassen +#### Positions- und Lamellennachführung Diese Einstellung ist nur für den Gerätetype "Jalousie" vorhanden. - **Nein**: Die Lamellenstellung wird nicht an den Sonnenstand angepasst. Stattdessen wird die unter "Lamellenstellung" konfigurierte feste Position verwendet. - **Standard**: Die Lamellenstellung wird anhand des Höhenwinkels der Sonne automatisch berechnet (bewährte Formel). -- **Benutzerdefiniert**: Die Lamellenstellung wird linear zwischen dem Wert bei tiefem Sonnenstand und dem Wert bei hohem Sonnenstand interpoliert. +- **Lamellenführung (Min/Max)**: Wie "Standard", jedoch wird die berechnete Lamellenstellung auf konfigurierbare Min./Max.-Grenzen begrenzt. +- **Geo. Positionsnachführung**: Berechnet die Jalousieposition geometrisch anhand der Sonneneindringtiefe. Die Lamelle wird auf eine feste konfigurierte Stellung gesetzt. +- **Geo. Positionsnachführung (Min/Max)**: Wie "Geo. Positionsnachführung", jedoch wird die berechnete Position auf konfigurierbare Min./Max.-Grenzen begrenzt. +- **Geo. Positions- und Lamellennachführung**: Berechnet sowohl Position als auch Lamellenstellung geometrisch anhand der Sonnengeometrie. +- **Geo. Positions- und Lamellennachführung (Min/Max)**: Wie "Geo. Positions- und Lamellennachführung", jedoch werden Position und Lamellenstellung auf konfigurierbare Min./Max.-Grenzen begrenzt. + + +#### Betriebsart Lamellennachführung + + +Diese Einstellung ist nur vorhanden, wenn unter "Positions- und Lamellennachführung" "Geo. Positions- und Lamellennachführung" oder "Geo. Positions- und Lamellennachführung (Min/Max)" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. + +Legt fest, wie die Lamellenstellung automatisch berechnet wird: + +- **Tageslicht-optimiert**: Geometrische Berechnung des kritischen Kippwinkels θ, bei dem gerade kein direktes Sonnenlicht durch die Lamellen eindringt (Schattenkante). Hierfür werden Lamellenbreite und Lamellenabstand benötigt. +- **Blendschutz-optimiert**: Die Lamellen werden so gestellt, dass Sonnenstrahlen parallel reflektiert werden (θ = Profilwinkel). Maximaler Blendschutz ohne geometrische Kalibrierung. +- **Über Tabelle**: Die Lamellenstellung wird anhand einer konfigurierbaren Tabelle mit 6 Stützpunkten (Höhenwinkel → Position in %) per linearer Interpolation berechnet. Geeignet für herstellerspezifische Vorgaben (z. B. Warema-Tabellen). + + +#### Position bei Sonne unter min. Höhenwinkel (Tabelle) + + +Diese Einstellung ist nur vorhanden, wenn "Betriebsart Lamellennachführung" auf "Über Tabelle" gesetzt ist. + +Lamellenstellung (0–100%), die verwendet wird, wenn der Sonnen-Höhenwinkel unterhalb des konfigurierten Höhenwinkels von Stützpunkt 1 liegt. + + +#### Min. Höhenwinkel (Tabelle) + + +Diese Einstellung ist nur vorhanden, wenn "Betriebsart Lamellennachführung" auf "Über Tabelle" gesetzt ist. + +Minimaler Sonnenhöhenwinkel in Grad (°). Unterhalb dieses Werts wird die "Position bei Sonne unter min. Höhenwinkel" verwendet. + + +#### Höhenwinkel Stützpunkt (bis) + + +Diese Einstellung ist nur vorhanden, wenn "Betriebsart Lamellennachführung" auf "Über Tabelle" gesetzt ist. + +Sonnenhöhe in Grad (°) als Obergrenze dieses Intervalls. Ab diesem Wert gilt der Lamellenstellungswert des nächsten Stützpunkts. Die Werte müssen aufsteigend sortiert sein. + + +#### Höhenwinkel Stützpunkt 1...6 + + +Diese Einstellungen sind nur vorhanden, wenn "Betriebsart Lamellennachführung" auf "Über Tabelle" gesetzt ist. + +Sonnenhöhe in Grad (°) für jeden der 6 Stützpunkte der Tabelle. Die Werte müssen aufsteigend sortiert sein (Stützpunkt 1 ≤ 2 ≤ ... ≤ 6). Unterhalb von Stützpunkt 1 gilt die "Position bei Sonne unter min. Höhenwinkel", oberhalb von Stützpunkt 6 bleibt die Lamellenstellung konstant auf dem Wert von Stützpunkt 6. + + +#### Lamellenstellung Stützpunkt 1...6 + + +Diese Einstellungen sind nur vorhanden, wenn "Betriebsart Lamellennachführung" auf "Über Tabelle" gesetzt ist. + +Lamellenstellung (0–100%) beim jeweiligen Höhenwinkel-Stützpunkt. Zwischen zwei Stützpunkten wird die Lamellenstellung linear interpoliert. #### Lamellenstellung -Diese Einstellung ist nur vorhanden, wenn unter "Lamellenstellenung an Sonnenstand anpassen" "Nein" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. +Diese Einstellung ist nur vorhanden, wenn unter "Positions- und Lamellennachführung" "Nein" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. Der Wert gibt die Kippstellung der Lamelle in Prozent an. 50% entsprechen der waagrechten Stellung. @@ -834,7 +922,7 @@ Der Wert gibt die Kippstellung der Lamelle in Prozent an. 50% entsprechen der wa #### Mindestaenderung Lamellennachfuehrung -Diese Einstellung ist nur vorhanden, wenn unter "Lamellenstellenung an Sonnenstand anpassen" "Standard" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. +Diese Einstellung ist nur vorhanden, wenn unter "Positions- und Lamellennachführung" "Standard" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. Der Wert gibt an, wie oft die Lamellenstellung während des Sonnenverlaufs angepasst wird. @@ -842,7 +930,7 @@ Der Wert gibt an, wie oft die Lamellenstellung während des Sonnenverlaufs angep #### Lamellenstellung bei min. Höhenwinkel -Diese Einstellung ist nur vorhanden, wenn unter "Lamellenstellenung an Sonnenstand anpassen" "Benutzerdefiniert" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. +Diese Einstellung ist nur vorhanden, wenn unter "Positions- und Lamellennachführung" "Lamellenführung (Min/Max)" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. Lamellenposition in Prozent beim minimalen Höhenwinkel der Sonne. Typischerweise nahezu geschlossen (z.B. 80%), da der flache Sonnenstand mehr Blendschutz erfordert. @@ -850,7 +938,7 @@ Lamellenposition in Prozent beim minimalen Höhenwinkel der Sonne. Typischerweis #### Lamellenstellung bei max. Höhenwinkel -Diese Einstellung ist nur vorhanden, wenn unter "Lamellenstellenung an Sonnenstand anpassen" "Benutzerdefiniert" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. +Diese Einstellung ist nur vorhanden, wenn unter "Positions- und Lamellennachführung" "Lamellenführung (Min/Max)" eingestellt wurde und der Gerätetype "Jalousie" verwendet wird. Lamellenposition in Prozent beim maximalen Höhenwinkel der Sonne. Typischerweise offener (z.B. 50% = waagrecht), da der steile Sonnenstand weniger Kippwinkel benötigt. @@ -862,6 +950,117 @@ Zwischen diesen beiden Werten wird die Lamellenstellung linear interpoliert. Hat die Jalousie sehr breite oder sehr schmale Lamellenblätter oder ist das Fenster nicht senkrecht verbaut, kann es notwendig sein zum errechnete Wert der Jalousiennachführung einen zusätzlichen Kippwinkel-Offset anzugeben. Der Kippwinkel-Offset kann positiv oder negativ sein um mehr oder weniger zu kippen. + +#### Lamellenbreite + +Die physische Breite einer einzelnen Lamelle in Millimetern (gemessen quer zur Lamellenachse). + +Dieser Wert wird für die geometrische Berechnung des optimalen Kippwinkels bei der **Lamellennachführung (Experte)** benötigt. Den Wert finden Sie in der technischen Dokumentation des Jalousie-Herstellers. + + +#### Lamellenabstand + +Der Abstand zwischen zwei benachbarten Lamellenachsen in Millimetern (Achsmaß). + +Dieser Wert wird zusammen mit der Lamellenbreite für die geometrische Berechnung des kritischen Kippwinkels verwendet, ab dem die Lamellen den Lichtstrahl vollständig sperren. Den Wert finden Sie in der technischen Dokumentation des Jalousie-Herstellers. + + +#### Lamellenwinkel vollständig geöffnet (0%) + +Der physische Winkel der Lamellen in Grad, wenn der Aktor den Stellwert 0% empfängt. + +Dieser Kalibrierungswert wird benötigt, damit die berechnete optimale Lamellenstellung korrekt auf den vom Aktor erwarteten Prozentwert abgebildet wird. Verschiedene Aktoren und Montagevarianten können unterschiedliche Konventionen verwenden. + + +#### Lamellenwinkel vollständig geschlossen (100%) + +Der physische Winkel der Lamellen in Grad, wenn der Aktor den Stellwert 100% empfängt. + +**Hinweis:** Ein Wert kleiner als "Lamellenwinkel vollständig geöffnet (0%)" ist ausdrücklich erlaubt und notwendig für Lamellen, die umgekehrt montiert sind. Die Nachführungsformel invertiert das Mapping automatisch. + + +#### Fensterhöhe + +Die lichte Höhe der Fensterscheibe in Zentimetern (Innenkante Blendrahmen oben bis Innenkante Blendrahmen unten bzw. Fensterbank). + +Dieser Wert wird für die Berechnung der optimalen Jalousie- oder Rolladenposition bei der **Geometrischen Positionsnachführung** benötigt. Zusammen mit der aktuellen Sonnenposition wird berechnet, wie weit der Behang abgesenkt sein muss, damit die Schattenkante die konfigurierte Eindringtiefe nicht überschreitet. + + +#### Max. Eindringtiefe + +Die maximale Sonneneindringtiefe im Raum in Zentimetern, gemessen ab der Fensterscheibe (Innenmaß). + +Wenn die Sonne tiefer als dieser Wert in den Raum eindringen würde, schließt die Jalousie oder der Rollladen weiter, bis die Schattenkante genau auf der konfigurierten Tiefe liegt. Bei sehr flachem Sonnenstand (Schattenkante wäre jenseits der Fenstertiefe) wird der Behang vollständig geschlossen. + + +#### Mindestverschiebung Schattenkante + +Mindestverschiebung der berechneten Schattenkante in Zentimetern, bevor ein neuer Positionsbefehl an den Aktor gesendet wird. + +Verhindert zu häufige kleine Positionskorrekturen bei langsam wandernder Sonne. Ein Wert von 5–10 cm ist für die meisten Anwendungen empfehlenswert. + + +#### Brüstungshöhe + +Höhe der Fensterbrüstung in Zentimetern, gemessen vom Fußboden bis zur Unterkante der Fensterscheibe. + +Dieser Wert wird zusammen mit der Fensterhöhe verwendet, um die absolute Position der Schattenkante im Raum zu bestimmen. Bei bodentiefen Fenstern (z.B. Terrassentür bis zum Boden) ist dieser Wert 0. + + +#### Positionsnachführung + +Aktiviert die dynamische Positionsnachführung für Rollläden basierend auf der aktuellen Sonnenposition. + +- **Nein**: Keine Positionsnachführung. Der Rollladen fährt auf die konfigurierte Beschattungsposition und bleibt dort. +- **Geo. Positionsnachführung**: Die Rolladenposition wird kontinuierlich berechnet, sodass die Sonneneindringtiefe die konfigurierte maximale Eindringtiefe nicht überschreitet. Die Position ändert sich dynamisch mit dem Sonnenverlauf. +- **Geo. Positionsnachführung (Min/Max)**: Wie "Geo. Positionsnachführung", jedoch wird die berechnete Position auf konfigurierbare Min./Max.-Grenzen begrenzt. + +Siehe auch: **Beschattungsstart** – steuert, ob der Behang beim Start sofort auf die Beschattungsposition fährt oder erst wartet, bis die Sonne geometrisch auf die Fassade trifft. + + +#### Min. Position + + +Diese Einstellung ist nur vorhanden, wenn unter "Positions- und Lamellennachführung" "Geo. Positionsnachführung (Min/Max)" oder "Geo. Positions- und Lamellennachführung (Min/Max)" eingestellt wurde, oder wenn unter "Positionsnachführung" "Geo. Positionsnachführung (Min/Max)" eingestellt wurde. + +Minimale Position in Prozent. Die dynamisch berechnete Position wird niemals unter diesen Wert abgesenkt. + +Typischer Anwendungsfall: Min. Position = 20% → Die Jalousie ist immer mindestens zu 20% geschlossen, unabhängig vom Sonnenstand. So bleibt ein minimaler Sichtschutz immer erhalten. + +Wenn Min. Position größer als Max. Position konfiguriert ist, wird die Begrenzung ignoriert und das Berechnungsergebnis unverändert ausgegeben. + + +#### Max. Position + + +Diese Einstellung ist nur vorhanden, wenn unter "Positions- und Lamellennachführung" "Geo. Positionsnachführung (Min/Max)" oder "Geo. Positions- und Lamellennachführung (Min/Max)" eingestellt wurde, oder wenn unter "Positionsnachführung" "Geo. Positionsnachführung (Min/Max)" eingestellt wurde. + +Maximale Position in Prozent. Die dynamisch berechnete Position wird niemals über diesen Wert erhöht. + +Typischer Anwendungsfall: Max. Position = 80% → Die Jalousie schließt nie vollständig zu. Immer etwas Tageslicht bleibt erhalten, und die Lamellenstellung übernimmt den Blend- und Wärmeschutz. + +Wenn Min. Position größer als Max. Position konfiguriert ist, wird die Begrenzung ignoriert und das Berechnungsergebnis unverändert ausgegeben. + +Hinweis: Die Begrenzung gilt **nicht** für die Beschattungsposition, die beim Start angefahren wird (Schutzposition). Nur die dynamisch berechneten Geo-Nachführungs-Positionen werden begrenzt. + + +#### Lamellenstellung bei min. Höhenwinkel (Begrenzung) + + +In diesem Modus dient dieser Parameter als **untere Begrenzung** der Lamellenstellung. Die dynamisch berechnete Lamellenstellung wird niemals unter den kleineren der beiden Werte (min./max. Höhenwinkel) abgesenkt. + +Wenn beide Werte gleich sind, wird die Lamellenstellung auf diesen festen Wert fixiert. + + +#### Lamellenstellung bei max. Höhenwinkel (Begrenzung) + + +In diesem Modus dient dieser Parameter als **obere Begrenzung** der Lamellenstellung. Die dynamisch berechnete Lamellenstellung wird niemals über den größeren der beiden Werte (min./max. Höhenwinkel) erhöht. + +Die Reihenfolge der beiden Werte (welcher ist kleiner, welcher größer) ist unerheblich – das Clipping verwendet automatisch min(Wert1, Wert2) als untere und max(Wert1, Wert2) als obere Grenze. + +Standardwerte sind 80%/50% (sinnvoll für Modus "Benutzerdefiniert"). Für kein Lamellen-Clipping: 0% und 100% konfigurieren. + ### Temperaturgrenzen @@ -1068,6 +1267,8 @@ Bit 19: Temperatur zu niedrig Bit 20: Vorhergesagte Temperatur zu niedrig Bit 21: Bewölkungsgrad zu hoch Bit 22: UV-Index zu niedrig +Bit 23: Profilwinkel nicht berechenbar (Sonne trifft Fassade nicht) +Bit 24: Flachdach-Schutz aktiv (Fassadenneigung zu gering) ### 'Nicht erlaubt' Grund @@ -1099,6 +1300,8 @@ Gibt es mehr als einen Grund, wird der erste dieser Liste angezeigt. 21: Vorhergesagte Temperatur zu niedrig 22: Bewölkungsgrad zu hoch 23: UV-Index zu niedrig +24: Profilwinkel nicht berechenbar (Sonne trifft Fassade nicht) +25: Flachdach-Schutz aktiv (Fassadenneigung zu gering) diff --git a/library.json b/library.json index b72b987..07a8919 100755 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "OFM-ShutterControllerModule", - "version": "0.6.0", + "version": "0.6.4", "dependencies": { }, diff --git a/src/Baggages/Help_de/SHC-Azimut-Sensor-1.md b/src/Baggages/Help_de/SHC-Azimut-Sensor-1.md deleted file mode 100644 index 7a5bff5..0000000 --- a/src/Baggages/Help_de/SHC-Azimut-Sensor-1.md +++ /dev/null @@ -1,5 +0,0 @@ -### 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 deleted file mode 100644 index ae32292..0000000 --- a/src/Baggages/Help_de/SHC-Azimut-Sensor-2.md +++ /dev/null @@ -1,5 +0,0 @@ -### 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 deleted file mode 100644 index 78cf245..0000000 --- a/src/Baggages/Help_de/SHC-Azimut-Sensor-3.md +++ /dev/null @@ -1,5 +0,0 @@ -### 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 deleted file mode 100644 index 71e5fb0..0000000 --- a/src/Baggages/Help_de/SHC-Azimut-Sensor-4.md +++ /dev/null @@ -1,5 +0,0 @@ -### 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 deleted file mode 100644 index be968e2..0000000 --- a/src/Baggages/Help_de/SHC-Azimut-Sensor-5.md +++ /dev/null @@ -1,5 +0,0 @@ -### 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-Beschattungsposition.md b/src/Baggages/Help_de/SHC-Beschattungsposition.md index 1f406e5..751cd6b 100644 --- a/src/Baggages/Help_de/SHC-Beschattungsposition.md +++ b/src/Baggages/Help_de/SHC-Beschattungsposition.md @@ -2,3 +2,9 @@ Position die bei Beschattungsstart angefahren wird. +Bei den Modi **Geometrische Positionsnachführung**, **Geometrische Positions- und Lamellennachführung** und **Geo. Positions- und Lamellennachführung (Min/Max)** wird die Position normalerweise dynamisch anhand des Sonnenstands berechnet. Die Beschattungsposition wirkt in diesen Modi nur als **Fallback**: Wenn die Sonne so flach steht, dass die berechnete Schattenkante oberhalb der Fensterhöhe liegt (d.h. der Behang muss vollständig abgesenkt sein), wird die hier konfigurierte Position als Zielwert verwendet. + +**Schutzposition:** Bei Beschattungsstart kann die Sonne zwar im konfigurierten Azimut- und Helligkeitsfenster liegen, aber geometrisch noch nicht auf die Fassade treffen (z.B. Westfenster, Sonne kommt noch aus Südosten). In diesem Fall fährt der Behang sofort auf die Beschattungsposition als Schutz gegen indirektes Licht und Reflexionen. Sobald die Sonne die Fassade trifft, übernimmt die geometrische Nachführung. + +Hinweis: Die Beschattungsposition wird beim Start **nicht** durch Min./Max.-Begrenzungen eingeschränkt. Soll dieses Verhalten vermieden werden, kann unter **Beschattungsstart** die Option "Nur bei direktem Sonnenlicht" gewählt werden. + diff --git a/src/Baggages/Help_de/SHC-Beschattungsstart-Modus.md b/src/Baggages/Help_de/SHC-Beschattungsstart-Modus.md new file mode 100644 index 0000000..5990bfc --- /dev/null +++ b/src/Baggages/Help_de/SHC-Beschattungsstart-Modus.md @@ -0,0 +1,14 @@ +### Beschattungsstart + +Legt fest, unter welcher Bedingung die Beschattung gestartet wird, wenn alle Mess- und Sonnenwert-Freigaben erfüllt sind. Diese Einstellung ist nur bei aktivem Geo-Tracking sichtbar (Jalousie: Modi 3–6; Rollo: Modi 1–2). + +**Sofort (mit Schutzposition)** (Standard): +Die Beschattung startet sofort, sobald Azimut, Elevation und Helligkeitswerte die konfigurierten Grenzen erfüllen. Es ist dabei möglich, dass die Sonne geometrisch noch nicht direkt auf die Fassade trifft (z.B. beim Westfenster kommt die Sonne zunächst noch aus Südosten). In diesem Fall fährt der Behang unmittelbar auf die konfigurierte **Beschattungsposition** als Schutz gegen indirektes Licht und Reflexionen. Sobald die Sonne die Fassade trifft (Profilwinkel > 0°), übernimmt die geometrische Nachführung die Positionsberechnung. + +**Nur bei direktem Sonnenlicht**: +Die Beschattung startet erst, wenn die Sonne sowohl alle Mess- und Sonnenwert-Bedingungen erfüllt als auch geometrisch auf die Fassade trifft (Profilwinkel > 0°). Solange die Sonne noch nicht auf die Fassade trifft, bleibt der Behang geöffnet – die Beschattungsposition wird nicht angefahren. + +**Beispiel Westfenster** (Azimut 120°–290°): Die Sonne trifft eine Westfassade erst ab ca. 180° Azimut. +- *Sofort*: Behang fährt ab 120° auf Beschattungsposition, geometrische Nachführung ab ~180°. +- *Nur bei direktem Sonnenlicht*: Behang bleibt bis ~180° vollständig geöffnet. + diff --git a/src/Baggages/Help_de/SHC-Betriebsart-Lamellennachfuehrung.md b/src/Baggages/Help_de/SHC-Betriebsart-Lamellennachfuehrung.md new file mode 100644 index 0000000..146db74 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Betriebsart-Lamellennachfuehrung.md @@ -0,0 +1,9 @@ +### Betriebsart Lamellennachführung + +Legt fest, wie die Lamellenstellung automatisch berechnet wird: + +- **Direktlicht abschneiden (Cut-Off)**: Geometrische Berechnung des kritischen Kippwinkels θ, bei dem gerade kein direktes Sonnenlicht durch die Lamellen eindringt (Schattenkante). Hierfür werden Lamellenbreite und Lamellenabstand benötigt. +- **Blendschutz (Retro-Reflexion)**: Die Lamellen werden so gestellt, dass Sonnenstrahlen parallel reflektiert werden (θ = 90° − Profilwinkel). Maximaler Blendschutz ohne geometrische Kalibrierung. +- **Adaptiv (Daylight)**: Kombiniert Cut-Off und Retro-Reflexion automatisch. Wählt jeweils die offenere Lamellenstellung (θ = min(θ_CutOff, θ_Retro)). Maximale Tageslichtnutzung bei gleichzeitigem Blendschutz — der Wechsel zwischen beiden Betriebsarten erfolgt am geometrischen Schnittpunkt, sprungfrei und ohne zusätzliche Parameter. +- **Über Tabelle**: Die Lamellenstellung wird anhand einer konfigurierbaren Tabelle mit 6 Stützpunkten (Höhenwinkel → Position in %) per linearer Interpolation berechnet. Geeignet für herstellerspezifische Vorgaben (z. B. Warema-Tabellen). + diff --git a/src/Baggages/Help_de/SHC-Bis-Hoehenwinkel.md b/src/Baggages/Help_de/SHC-Bis-Hoehenwinkel.md new file mode 100644 index 0000000..2168d2d --- /dev/null +++ b/src/Baggages/Help_de/SHC-Bis-Hoehenwinkel.md @@ -0,0 +1,4 @@ +### Höhenwinkel Stützpunkt (bis) + +Sonnenhöhe in Grad (°) als Obergrenze dieses Intervalls. Ab diesem Wert gilt der Lamellenstellungswert des nächsten Stützpunkts. Die Werte müssen aufsteigend sortiert sein. + diff --git a/src/Baggages/Help_de/SHC-Bruestungshoehe.md b/src/Baggages/Help_de/SHC-Bruestungshoehe.md new file mode 100644 index 0000000..ba07d43 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Bruestungshoehe.md @@ -0,0 +1,6 @@ +### Brüstungshöhe + +Höhe der Fensterbrüstung in Zentimetern, gemessen vom Fußboden bis zur Unterkante der Fensterscheibe. + +Dieser Wert wird zusammen mit der Fensterhöhe verwendet, um die absolute Position der Schattenkante im Raum zu bestimmen. Bei bodentiefen Fenstern (z.B. Terrassentür bis zum Boden) ist dieser Wert 0. + diff --git a/src/Baggages/Help_de/SHC-FassadenneigungFensterneigung.md b/src/Baggages/Help_de/SHC-FassadenneigungFensterneigung.md new file mode 100644 index 0000000..a44a3ca --- /dev/null +++ b/src/Baggages/Help_de/SHC-FassadenneigungFensterneigung.md @@ -0,0 +1,10 @@ +### Fassadenneigung/Fensterneigung + +Die Neigung der Fassade oder eines Dachfenster in Grad, gemessen gegenüber der Senkrechten (0° = senkrechte Wand). + +- **0°**: Senkrechte Fassade/Senkrechtes Fenster (Standardfall) +- **Positiver Wert**: Fassade/Fenster neigt sich nach außen (z.B. überhängende Dachkante) +- **Negativer Wert**: Fassade/Fenster neigt sich nach innen (z.B. nach innen geneigte Wand) + +Dieser Wert wird für die Berechnung des Profilwinkels bei der **Geometrischen Positionsnachführung** und der **Geo. Positions- und Lamellennachführung** verwendet. Bei senkrechten Fenstern kann der Standardwert 0° belassen werden. + 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 index b235bdd..71ac5d1 100644 --- a/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung-und-Azimut-Auswertung.md +++ b/src/Baggages/Help_de/SHC-Fenster-Behangausrichtung-und-Azimut-Auswertung.md @@ -15,6 +15,6 @@ Beispiele (vereinfachte Sicht): | 3x Sensor mit Azimut (O/S/W) | Dachflaeche | Max(alle 3 Sensoren) — Fallback, da kein unzugeordneter Sensor | | 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Dachflaeche | Max(nur der Sensor ohne Azimut) | | 4x Sensor mit Azimut + 1x Sensor ohne Azimut | Sued | Azimut-Interpolation nur mit den 4 Azimut-Sensoren | -| 2x Sensor ohne Azimut | Keine Himmelsrichtungsauswertung | Aggregation ueber alle Sensoren ohne Azimut | -| 1x Sensor mit Azimut | Keine Himmelsrichtungsauswertung | Aggregation ueber alle gueltigen Sensoren | +| 2x Sensor ohne Azimut | Keine Himmelsrichtungsauswertung | Aggregation über alle Sensoren ohne Azimut | +| 1x Sensor mit Azimut | Keine Himmelsrichtungsauswertung | Aggregation über alle gueltigen Sensoren | diff --git a/src/Baggages/Help_de/SHC-Fensterhoehe.md b/src/Baggages/Help_de/SHC-Fensterhoehe.md new file mode 100644 index 0000000..ada9486 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Fensterhoehe.md @@ -0,0 +1,6 @@ +### Fensterhöhe + +Die lichte Höhe der Fensterscheibe in Zentimetern (Innenkante Blendrahmen oben bis Innenkante Blendrahmen unten bzw. Fensterbank). + +Dieser Wert wird für die Berechnung der optimalen Jalousie- oder Rolladenposition bei der **Geometrischen Positionsnachführung** benötigt. Zusammen mit der aktuellen Sonnenposition wird berechnet, wie weit der Behang abgesenkt sein muss, damit die Schattenkante die konfigurierte Eindringtiefe nicht überschreitet. + diff --git a/src/Baggages/Help_de/SHC-Helligkeit.md b/src/Baggages/Help_de/SHC-Helligkeit.md deleted file mode 100644 index a9f60d5..0000000 --- a/src/Baggages/Help_de/SHC-Helligkeit.md +++ /dev/null @@ -1,4 +0,0 @@ -### Helligkeit - -Vorgesehen für den Helligkeitswert einer KNX-Wetterstation. - diff --git a/src/Baggages/Help_de/SHC-Hoehenwinkel-Stuetzpunkt-1-6.md b/src/Baggages/Help_de/SHC-Hoehenwinkel-Stuetzpunkt-1-6.md new file mode 100644 index 0000000..7a9cca3 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Hoehenwinkel-Stuetzpunkt-1-6.md @@ -0,0 +1,4 @@ +### Höhenwinkel Stützpunkt 1...6 + +Sonnenhöhe in Grad (°) für jeden der 6 Stützpunkte der Tabelle. Die Werte müssen aufsteigend sortiert sein (Stützpunkt 1 ≤ 2 ≤ ... ≤ 6). Unterhalb von Stützpunkt 1 gilt die "Position bei Sonne unter min. Höhenwinkel", oberhalb von Stützpunkt 6 bleibt die Lamellenstellung konstant auf dem Wert von Stützpunkt 6. + diff --git a/src/Baggages/Help_de/SHC-Hoehenwinkel.md b/src/Baggages/Help_de/SHC-Hoehenwinkel.md new file mode 100644 index 0000000..62c9544 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Hoehenwinkel.md @@ -0,0 +1,4 @@ +### Min. Höhenwinkel (Tabelle) + +Minimaler Sonnenhöhenwinkel in Grad (°). Unterhalb dieses Werts wird die "Position bei Sonne unter min. Höhenwinkel" verwendet. + diff --git a/src/Baggages/Help_de/SHC-Lamelle-ist-waagrecht-bei.md b/src/Baggages/Help_de/SHC-Lamelle-ist-waagrecht-bei.md new file mode 100644 index 0000000..ef2e071 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Lamelle-ist-waagrecht-bei.md @@ -0,0 +1,6 @@ +### Lamelle ist waagrecht bei + + +- **0%**: 0% = waagrecht (herstellerspezifisch, selten). +- **50%**: 50% = waagrecht (KNX-Standard, Vorgabewert). + diff --git a/src/Baggages/Help_de/SHC-Lamellenabstand.md b/src/Baggages/Help_de/SHC-Lamellenabstand.md new file mode 100644 index 0000000..1ffb9b9 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Lamellenabstand.md @@ -0,0 +1,6 @@ +### Lamellenabstand + +Der Abstand zwischen zwei benachbarten Lamellenachsen in Millimetern (Achsmaß). + +Dieser Wert wird zusammen mit der Lamellenbreite für die geometrische Berechnung des kritischen Kippwinkels verwendet, ab dem die Lamellen den Lichtstrahl vollständig sperren. Den Wert finden Sie in der technischen Dokumentation des Jalousie-Herstellers. + diff --git a/src/Baggages/Help_de/SHC-Lamellenbreite.md b/src/Baggages/Help_de/SHC-Lamellenbreite.md new file mode 100644 index 0000000..c8d6706 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Lamellenbreite.md @@ -0,0 +1,6 @@ +### Lamellenbreite + +Die physische Breite einer einzelnen Lamelle in Millimetern (gemessen quer zur Lamellenachse). + +Dieser Wert wird für die geometrische Berechnung des optimalen Kippwinkels bei der **Lamellennachführung (Experte)** benötigt. Den Wert finden Sie in der technischen Dokumentation des Jalousie-Herstellers. + diff --git a/src/Baggages/Help_de/SHC-Lamellenstellenung-an-Sonnenstand-anpassen.md b/src/Baggages/Help_de/SHC-Lamellenstellenung-an-Sonnenstand-anpassen.md deleted file mode 100644 index dec8b7f..0000000 --- a/src/Baggages/Help_de/SHC-Lamellenstellenung-an-Sonnenstand-anpassen.md +++ /dev/null @@ -1,8 +0,0 @@ -### Lamellenstellenung an Sonnenstand anpassen - -Diese Einstellung ist nur für den Gerätetype "Jalousie" vorhanden. - -- **Nein**: Die Lamellenstellung wird nicht an den Sonnenstand angepasst. Stattdessen wird die unter "Lamellenstellung" konfigurierte feste Position verwendet. -- **Standard**: Die Lamellenstellung wird anhand des Höhenwinkels der Sonne automatisch berechnet (bewährte Formel). -- **Benutzerdefiniert**: Die Lamellenstellung wird linear zwischen dem Wert bei tiefem Sonnenstand und dem Wert bei hohem Sonnenstand interpoliert. - diff --git a/src/Baggages/Help_de/SHC-Lamellenstellung-bei-max-Hoehenwinkel-Begrenzung.md b/src/Baggages/Help_de/SHC-Lamellenstellung-bei-max-Hoehenwinkel-Begrenzung.md new file mode 100644 index 0000000..5bdfa6b --- /dev/null +++ b/src/Baggages/Help_de/SHC-Lamellenstellung-bei-max-Hoehenwinkel-Begrenzung.md @@ -0,0 +1,6 @@ +### Lamellenstellung bei max. Höhenwinkel (Begrenzung) + +Die Reihenfolge der beiden Werte (welcher ist kleiner, welcher größer) ist unerheblich – das Clipping verwendet automatisch min(Wert1, Wert2) als untere und max(Wert1, Wert2) als obere Grenze. + +Standardwerte sind 80%/50% (sinnvoll für Modus "Benutzerdefiniert"). Für kein Lamellen-Clipping: 0% und 100% konfigurieren. + diff --git a/src/Baggages/Help_de/SHC-Lamellenstellung-bei-min-Hoehenwinkel-Begrenzung.md b/src/Baggages/Help_de/SHC-Lamellenstellung-bei-min-Hoehenwinkel-Begrenzung.md new file mode 100644 index 0000000..582fe00 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Lamellenstellung-bei-min-Hoehenwinkel-Begrenzung.md @@ -0,0 +1,4 @@ +### Lamellenstellung bei min. Höhenwinkel (Begrenzung) + +Wenn beide Werte gleich sind, wird die Lamellenstellung auf diesen festen Wert fixiert. + diff --git a/src/Baggages/Help_de/SHC-Lamellenwinkel-vollstaendig-geoeffnet-0.md b/src/Baggages/Help_de/SHC-Lamellenwinkel-vollstaendig-geoeffnet-0.md new file mode 100644 index 0000000..ab1b972 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Lamellenwinkel-vollstaendig-geoeffnet-0.md @@ -0,0 +1,6 @@ +### Lamellenwinkel vollständig geöffnet (0%) + +Der physische Winkel der Lamellen in Grad, wenn der Aktor den Stellwert 0% empfängt. + +Dieser Kalibrierungswert wird benötigt, damit die berechnete optimale Lamellenstellung korrekt auf den vom Aktor erwarteten Prozentwert abgebildet wird. Verschiedene Aktoren und Montagevarianten können unterschiedliche Konventionen verwenden. + diff --git a/src/Baggages/Help_de/SHC-Lamellenwinkel-vollstaendig-geschlossen-100.md b/src/Baggages/Help_de/SHC-Lamellenwinkel-vollstaendig-geschlossen-100.md new file mode 100644 index 0000000..9aafd27 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Lamellenwinkel-vollstaendig-geschlossen-100.md @@ -0,0 +1,6 @@ +### Lamellenwinkel vollständig geschlossen (100%) + +Der physische Winkel der Lamellen in Grad, wenn der Aktor den Stellwert 100% empfängt. + +**Hinweis:** Ein Wert kleiner als "Lamellenwinkel vollständig geöffnet (0%)" ist ausdrücklich erlaubt und notwendig für Lamellen, die umgekehrt montiert sind. Die Nachführungsformel invertiert das Mapping automatisch. + diff --git a/src/Baggages/Help_de/SHC-Max-Eindringtiefe.md b/src/Baggages/Help_de/SHC-Max-Eindringtiefe.md new file mode 100644 index 0000000..c2ac20f --- /dev/null +++ b/src/Baggages/Help_de/SHC-Max-Eindringtiefe.md @@ -0,0 +1,6 @@ +### Max. Eindringtiefe + +Die maximale Sonneneindringtiefe im Raum in Zentimetern, gemessen ab der Fensterscheibe (Innenmaß). + +Wenn die Sonne tiefer als dieser Wert in den Raum eindringen würde, schließt die Jalousie oder der Rollladen weiter, bis die Schattenkante genau auf der konfigurierten Tiefe liegt. Bei sehr flachem Sonnenstand (Schattenkante wäre jenseits der Fenstertiefe) wird der Behang vollständig geschlossen. + diff --git a/src/Baggages/Help_de/SHC-Max-Position-Begrenzung.md b/src/Baggages/Help_de/SHC-Max-Position-Begrenzung.md new file mode 100644 index 0000000..0bf0476 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Max-Position-Begrenzung.md @@ -0,0 +1,10 @@ +### Max. Position + +Maximale Position in Prozent. Die dynamisch berechnete Position wird niemals über diesen Wert erhöht. + +Typischer Anwendungsfall: Max. Position = 80% → Die Jalousie schließt nie vollständig zu. Immer etwas Tageslicht bleibt erhalten, und die Lamellenstellung übernimmt den Blend- und Wärmeschutz. + +Wenn Min. Position größer als Max. Position konfiguriert ist, wird die Begrenzung ignoriert und das Berechnungsergebnis unverändert ausgegeben. + +Hinweis: Die Begrenzung gilt **nicht** für die Beschattungsposition, die beim Start angefahren wird (Schutzposition). Nur die dynamisch berechneten Geo-Nachführungs-Positionen werden begrenzt. + diff --git a/src/Baggages/Help_de/SHC-Min-Position-Begrenzung.md b/src/Baggages/Help_de/SHC-Min-Position-Begrenzung.md new file mode 100644 index 0000000..d8785bc --- /dev/null +++ b/src/Baggages/Help_de/SHC-Min-Position-Begrenzung.md @@ -0,0 +1,8 @@ +### Min. Position + +Minimale Position in Prozent. Die dynamisch berechnete Position wird niemals unter diesen Wert abgesenkt. + +Typischer Anwendungsfall: Min. Position = 20% → Die Jalousie ist immer mindestens zu 20% geschlossen, unabhängig vom Sonnenstand. So bleibt ein minimaler Sichtschutz immer erhalten. + +Wenn Min. Position größer als Max. Position konfiguriert ist, wird die Begrenzung ignoriert und das Berechnungsergebnis unverändert ausgegeben. + diff --git a/src/Baggages/Help_de/SHC-Mindestverschiebung-Schattenkante.md b/src/Baggages/Help_de/SHC-Mindestverschiebung-Schattenkante.md new file mode 100644 index 0000000..afdf0a9 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Mindestverschiebung-Schattenkante.md @@ -0,0 +1,6 @@ +### Mindestverschiebung Schattenkante + +Mindestverschiebung der berechneten Schattenkante in Zentimetern, bevor ein neuer Positionsbefehl an den Aktor gesendet wird. + +Verhindert zu häufige kleine Positionskorrekturen bei langsam wandernder Sonne. Ein Wert von 5–10 cm ist für die meisten Anwendungen empfehlenswert. + diff --git a/src/Baggages/Help_de/SHC-Nicht-erlaubt-Bits-Nur-fuer-Experten.md b/src/Baggages/Help_de/SHC-Nicht-erlaubt-Bits-Nur-fuer-Experten.md index e36ea37..799fe55 100644 --- a/src/Baggages/Help_de/SHC-Nicht-erlaubt-Bits-Nur-fuer-Experten.md +++ b/src/Baggages/Help_de/SHC-Nicht-erlaubt-Bits-Nur-fuer-Experten.md @@ -26,4 +26,6 @@ Bit 19: Temperatur zu niedrig Bit 20: Vorhergesagte Temperatur zu niedrig Bit 21: Bewölkungsgrad zu hoch Bit 22: UV-Index zu niedrig +Bit 23: Profilwinkel nicht berechenbar (Sonne trifft Fassade nicht) +Bit 24: Flachdach-Schutz aktiv (Fassadenneigung zu gering) diff --git a/src/Baggages/Help_de/SHC-Nicht-erlaubt-Grund.md b/src/Baggages/Help_de/SHC-Nicht-erlaubt-Grund.md index 557b83e..f3a8131 100644 --- a/src/Baggages/Help_de/SHC-Nicht-erlaubt-Grund.md +++ b/src/Baggages/Help_de/SHC-Nicht-erlaubt-Grund.md @@ -27,5 +27,7 @@ Gibt es mehr als einen Grund, wird der erste dieser Liste angezeigt. 21: Vorhergesagte Temperatur zu niedrig 22: Bewölkungsgrad zu hoch 23: UV-Index zu niedrig +24: Profilwinkel nicht berechenbar (Sonne trifft Fassade nicht) +25: Flachdach-Schutz aktiv (Fassadenneigung zu gering) diff --git a/src/Baggages/Help_de/SHC-Position-Stuetzpunkt-1-6.md b/src/Baggages/Help_de/SHC-Position-Stuetzpunkt-1-6.md new file mode 100644 index 0000000..1be6f57 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Position-Stuetzpunkt-1-6.md @@ -0,0 +1,4 @@ +### Lamellenstellung Stützpunkt 1...6 + +Lamellenstellung (0–100%) beim jeweiligen Höhenwinkel-Stützpunkt. Zwischen zwei Stützpunkten wird die Lamellenstellung linear interpoliert. + diff --git a/src/Baggages/Help_de/SHC-Position-bei-Sonne-unter-min-Hoehenwinkel.md b/src/Baggages/Help_de/SHC-Position-bei-Sonne-unter-min-Hoehenwinkel.md new file mode 100644 index 0000000..9b64033 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Position-bei-Sonne-unter-min-Hoehenwinkel.md @@ -0,0 +1,4 @@ +### Position bei Sonne unter min. Höhenwinkel (Tabelle) + +Lamellenstellung (0–100%), die verwendet wird, wenn der Sonnen-Höhenwinkel unterhalb des konfigurierten Höhenwinkels von Stützpunkt 1 liegt. + diff --git a/src/Baggages/Help_de/SHC-Positions-und-Lamellennachfuehrung.md b/src/Baggages/Help_de/SHC-Positions-und-Lamellennachfuehrung.md new file mode 100644 index 0000000..90e3512 --- /dev/null +++ b/src/Baggages/Help_de/SHC-Positions-und-Lamellennachfuehrung.md @@ -0,0 +1,12 @@ +### Positions- und Lamellennachführung + +Diese Einstellung ist nur für den Gerätetype "Jalousie" vorhanden. + +- **Nein**: Die Lamellenstellung wird nicht an den Sonnenstand angepasst. Stattdessen wird die unter "Lamellenstellung" konfigurierte feste Position verwendet. +- **Standard**: Die Lamellenstellung wird anhand des Höhenwinkels der Sonne automatisch berechnet (bewährte Formel). +- **Lamellenführung (Min/Max)**: Wie "Standard", jedoch wird die berechnete Lamellenstellung auf konfigurierbare Min./Max.-Grenzen begrenzt. +- **Geo. Positionsnachführung**: Berechnet die Jalousieposition geometrisch anhand der Sonneneindringtiefe. Die Lamelle wird auf eine feste konfigurierte Stellung gesetzt. +- **Geo. Positionsnachführung (Min/Max)**: Wie "Geo. Positionsnachführung", jedoch wird die berechnete Position auf konfigurierbare Min./Max.-Grenzen begrenzt. +- **Geo. Positions- und Lamellennachführung**: Berechnet sowohl Position als auch Lamellenstellung geometrisch anhand der Sonnengeometrie. +- **Geo. Positions- und Lamellennachführung (Min/Max)**: Wie "Geo. Positions- und Lamellennachführung", jedoch werden Position und Lamellenstellung auf konfigurierbare Min./Max.-Grenzen begrenzt. + diff --git a/src/Baggages/Help_de/SHC-Positionsnachfuehrung.md b/src/Baggages/Help_de/SHC-Positionsnachfuehrung.md new file mode 100644 index 0000000..51d0c0d --- /dev/null +++ b/src/Baggages/Help_de/SHC-Positionsnachfuehrung.md @@ -0,0 +1,10 @@ +### Positionsnachführung + +Aktiviert die dynamische Positionsnachführung für Rollläden basierend auf der aktuellen Sonnenposition. + +- **Nein**: Keine Positionsnachführung. Der Rollladen fährt auf die konfigurierte Beschattungsposition und bleibt dort. +- **Geo. Positionsnachführung**: Die Rolladenposition wird kontinuierlich berechnet, sodass die Sonneneindringtiefe die konfigurierte maximale Eindringtiefe nicht überschreitet. Die Position ändert sich dynamisch mit dem Sonnenverlauf. +- **Geo. Positionsnachführung (Min/Max)**: Wie "Geo. Positionsnachführung", jedoch wird die berechnete Position auf konfigurierbare Min./Max.-Grenzen begrenzt. + +Siehe auch: **Beschattungsstart** – steuert, ob der Behang beim Start sofort auf die Beschattungsposition fährt oder erst wartet, bis die Sonne geometrisch auf die Fassade trifft. + diff --git a/src/Baggages/Help_de/SHC-Weitere-Helligkeitssensoren.md b/src/Baggages/Help_de/SHC-Weitere-Helligkeitssensoren.md deleted file mode 100644 index c5883d5..0000000 --- a/src/Baggages/Help_de/SHC-Weitere-Helligkeitssensoren.md +++ /dev/null @@ -1,5 +0,0 @@ -### 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 index c9f94e8..ef7430b 100644 --- a/src/BrightnessMeasurement.cpp +++ b/src/BrightnessMeasurement.cpp @@ -318,47 +318,28 @@ BrightnessMeasurement::ValueState BrightnessMeasurement::buildAzimuthState(float merged.push_back(entry); } - if (merged.size() == 1) + static constexpr float kDegToRad = 3.14159265358979323846f / 180.0f; + float weightSum = 0.0f; + float valueSum = 0.0f; + for (const auto& entry : merged) { - state.ignoreValue = false; - state.useFallback = false; - state.waitForValue = false; - state.valueLux = merged.front().valueLux; - return state; + float delta = sunAzimuth - entry.azimuth; + while (delta > 180.0f) delta -= 360.0f; + while (delta < -180.0f) delta += 360.0f; + float weight = std::cos(delta * kDegToRad); + if (weight <= 0.0f) + continue; + weightSum += weight; + valueSum += weight * entry.valueLux; } - 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; - } - } + if (weightSum < 1e-6f) + return aggregateState; state.ignoreValue = false; state.useFallback = false; state.waitForValue = false; - state.valueLux = result; + state.valueLux = valueSum / weightSum; return state; } diff --git a/src/CallContext.h b/src/CallContext.h index 1d6072f..735a2f6 100644 --- a/src/CallContext.h +++ b/src/CallContext.h @@ -15,6 +15,7 @@ class CallContext bool modeNewStarted = false; bool fastSimulationActive = false; bool isWindowOpenActive = false; + bool isWindowTiltActive = false; bool hasSlat = false; bool diagnosticLog = false; bool shadingControlActive = false; diff --git a/src/ModeNight.cpp b/src/ModeNight.cpp index a805139..f568c18 100644 --- a/src/ModeNight.cpp +++ b/src/ModeNight.cpp @@ -244,7 +244,7 @@ void ModeNight::stop(const CallContext &callContext, const ModeBase *next, Posit KoSHC_CNightActive.value(false, DPT_Switch); if (next != (const ModeBase *)callContext.modeManual && ParamSHC_CNightStopPositionEnabled) { - logDebugP("Set night stop position %d slat %d", (int) ParamSHC_CNightStartPosition, (int) ParamSHC_CNightStartSlatPosition); + logDebugP("Set night stop position %d slat %d", (int) ParamSHC_CNightStopPosition, (int) ParamSHC_CNightStopSlatPosition); positionController.setAutomaticPositionAndStoreForRestore(ParamSHC_CNightStopPosition); positionController.setAutomaticSlatAndStoreForRestore(ParamSHC_CNightStopSlatPosition); } diff --git a/src/ModeShading.cpp b/src/ModeShading.cpp index 6109a6d..85d9f25 100644 --- a/src/ModeShading.cpp +++ b/src/ModeShading.cpp @@ -2,6 +2,7 @@ #include "Timer.h" #include "PositionController.h" #include "MeasurementWatchdog.h" +#include #ifdef SHC_KoCShading2Active // redefine SHC_ParamCalcIndex to add offset for Shading Mode 2 @@ -270,6 +271,25 @@ bool ModeShading::allowed(const CallContext &callContext) _waitTimeAfterMeasurmentValueChange = 0; // stop wait time, because allowed again } } + else + { + // Mode is currently inactive (shading not running): + // When the measurement values become favorable again after a previous under-shoot + // within the same shading period (_needWaitTime == true), (re)start the start wait time + // so a short favorable spell does not immediately reactivate shading. + // Previously the start wait time was only armed in stop(); once that timer had elapsed + // while the values stayed unfavorable, the next favorable change reactivated shading + // instantly, so the configured "Beschattungsstart" wait time was effectively ignored. + if (_needWaitTime && allowedByMeasurementValuesAndHeatingOffWaitTime) + { + logDebugP("Restart starting wait time"); + _waitTimeAfterMeasurmentValueChange = callContext.currentMillis; // (re)activate start wait time + } + else if (!allowedByMeasurementValuesAndHeatingOffWaitTime) + { + _waitTimeAfterMeasurmentValueChange = 0; // values unfavorable again, cancel pending start wait time + } + } } // Handle start and stop wait time bool stopWaitTimeActive = false; @@ -361,6 +381,10 @@ bool ModeShading::allowed(const CallContext &callContext) return false; if (startWaitTimeActive) return false; + if (callContext.isWindowTiltActive && !windowTiltAllowed()) + return false; + if (callContext.isWindowOpenActive && !callContext.isWindowTiltActive && !windowOpenAllowed()) + return false; if (stopWaitTimeActive) return true; if (!allowedByHeatingOff) @@ -456,6 +480,33 @@ bool ModeShading::allowedBySun(const CallContext &callContext) _notAllowedReason |= ModeShadingNotAllowedReasonSunBreak; else _notAllowedReason &= ~ModeShadingNotAllowedReasonSunBreak; + + if (ParamSHC_CShading1RequireFacadeHit == 1) + { + bool isGeoTracking = (ParamSHC_CType == 2) + ? (ParamSHC_CShading1RolloPositionMode >= 1) + : (ParamSHC_CShading1SlatElevationDepending >= 3); + + const uint8_t orientation = ParamSHC_CWindowOrientation; + if (isGeoTracking && orientation != 5 && orientation != 6) + { + float beta = calculateProfileAngle( + (float)callContext.elevation, + (float)callContext.azimuth, + orientation, + (float)(int8_t)ParamSHC_CFacadeInclination); + if (beta <= 0.0f) + { + if (diagnosticLog) + logInfoP("RequireFacadeHit: Profilwinkel %.2f <= 0, nicht erlaubt", beta); + allowed = false; + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + } + else + _notAllowedReason &= ~ModeShadingNotAllowedReasonProfileAngleSentinel; + } + } + return allowed; } @@ -622,10 +673,78 @@ bool ModeShading::allowedByMeasurmentValues(const CallContext &callContext) #define SHC_KoCShading2Active SHC_KoCShading1Active #endif +// Berechnet den Profilwinkel [°] der Sonne bezogen auf die Fassadenebene. +// Rückgabe: Winkel in Grad [>0], oder -1.0f als Sentinel (Sonne trifft Fassade nicht / nicht berechenbar). +// Argumente: +// elevationDeg - Sonnenhöhe in Grad [0..90] +// azimuthDeg - Sonnenazimut in Grad [0..360, 0=Nord, 90=Ost, 180=Süd, 270=West] +// orientationEnum - WindowOrientation enum: 0=Ost, 1=Südost, 2=Süd, 3=Südwest, 4=West, 5=Dachfläche, 6=Keine +// facadeInclinationDeg - Fassadenneigung/Fensterneigung in Grad (0=senkrecht/Wand, positiv=nach vorne geneigt) +float ModeShading::calculateProfileAngle(float elevationDeg, float azimuthDeg, uint8_t orientationEnum, float facadeInclinationDeg) const +{ + static const float DEG2RAD = (float)M_PI / 180.0f; + static const float RAD2DEG = 180.0f / (float)M_PI; + static const float COS_EPSILON = 0.0175f; // cos(89°) ≈ 0.0175 + + if (orientationEnum == 6) + return -1.0f; // "Keine Himmelsrichtungsauswertung" → Modi 3-5 inaktiv + + if (orientationEnum == 5) + { + // Dachfläche: Profilwinkel = Höhenwinkel minus Dachneigung (von Horizontal) + float beta_deg = elevationDeg - facadeInclinationDeg; + return (beta_deg <= 0.0f) ? -1.0f : beta_deg; + } + + // Normalfall: Fassade mit Himmelsrichtung + // Mapping: Ost=90°, Südost=135°, Süd=180°, Südwest=225°, West=270° + static const float orientationAzimuth[] = {90.0f, 135.0f, 180.0f, 225.0f, 270.0f}; + float facadeAzimuth = orientationAzimuth[orientationEnum]; + + // Relativwinkel Sonne–Fassade, normiert auf [-180°, 180°] + float delta_phi = fmodf(azimuthDeg - facadeAzimuth + 180.0f, 360.0f) - 180.0f; + + // Guard 1: Sonne hinter der Fassade + if (fabsf(delta_phi) >= 90.0f) + return -1.0f; + + float cos_delta = cosf(delta_phi * DEG2RAD); + + // Guard 2: Sonne nahezu parallel zur Fassade (cos → 0) + if (fabsf(cos_delta) < COS_EPSILON) + return 89.9f; + + float elevation_rad = elevationDeg * DEG2RAD; + float facadeInclination_rad = facadeInclinationDeg * DEG2RAD; + + // Profilwinkel: Standardformel. facadeInclination korrigiert senkrechte Fassade → geneigte Fassade + float beta_rad = atanf(tanf(elevation_rad) / cos_delta) - facadeInclination_rad; + + // Guard 3: Vorneigung der Fassade übersteigt Sonnenhöhe → Sonne unterhalb Fassadenebene + if (beta_rad <= 0.0f) + return -1.0f; + + float beta_deg = beta_rad * RAD2DEG; + + // Guard 4: Cap bei 85° (tan(>85°) numerisch instabil) + if (beta_deg > 85.0f) + beta_deg = 85.0f; + + return beta_deg; +} + void ModeShading::start(const CallContext &callContext, const ModeBase *previous, PositionController &positionController) { _active = true; + _lastSentShadowPos = -1.0f; KoSHC_CShading1Active.value(true, DPT_Switch); + // Restore-Position nur beim ersten Shading-Start speichern (nicht bei Shading->Shading-Wechsel), + // damit "Position vor Beschattungsstart" die tatsächliche Position vor der gesamten Beschattungsphase ist. + if (previous == nullptr || !previous->isModeShading()) + { + positionController.storeCurrentPositionForRestore(); + positionController.setRestoreSlat(positionController.slat()); + } positionController.setAutomaticPosition(ParamSHC_CShading1ShadingPosition); // @@ -643,10 +762,99 @@ void ModeShading::control(const CallContext &callContext, PositionController &po // // // - if (!positionController.hasSlat()) + + // ── Rollo-Pfad: Geo. Positionsnachführung für Rollo (kein hasSlat!) ── + if (ParamSHC_CType == 2 && ParamSHC_CShading1RolloPositionMode >= 1) + { + static const float DEG2RAD = (float)M_PI / 180.0f; + _notAllowedReason &= ~(ModeShadingNotAllowedReasonProfileAngleSentinel | ModeShadingNotAllowedReasonFlatRoofGuard); + + float gamma_rad; + float sin_alpha; + const uint8_t orientation = ParamSHC_CWindowOrientation; + const float facadeInclination = (float)(int8_t)ParamSHC_CFacadeInclination; + + if (orientation == 5) + { + // Dachfläche: gamma = Höhenwinkel, sin_alpha = |sin(Dachneigung)| + float elev = (float)callContext.elevation; + if (elev <= 0.0f) + return; + sin_alpha = fabsf(sinf(facadeInclination * DEG2RAD)); + if (sin_alpha < 0.05f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonFlatRoofGuard; + return; // Flachdach — kein sinnvolles Positionstracking + } + gamma_rad = elev * DEG2RAD; + } + else + { + float beta_deg = calculateProfileAngle( + (float)callContext.elevation, + (float)callContext.azimuth, + orientation, + facadeInclination); + if (beta_deg <= 0.0f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + return; // Sonne trifft Fassade nicht + } + gamma_rad = beta_deg * DEG2RAD; + sin_alpha = 1.0f; // senkrechte Fassade: sin(90°) = 1 + } + + float windowHeight = (float)ParamSHC_CShading1WindowHeight; + float maxPenetration = (float)ParamSHC_CShading1MaxPenetrationDepth; + float windowSillHeight = (float)ParamSHC_CShading1WindowSillHeight; + float minShadowEdgeChange = (float)ParamSHC_CShading1MinShadowEdgeChange; + + float s_crit = (maxPenetration * tanf(gamma_rad) - windowSillHeight) / sin_alpha; + + float targetPos; + if (s_crit <= 0.0f) + targetPos = 100.0f; + else if (s_crit >= windowHeight) + targetPos = (float)ParamSHC_CShading1ShadingPosition; + else + targetPos = (1.0f - s_crit / windowHeight) * 100.0f; + + if (targetPos < 0.0f) targetPos = 0.0f; + if (targetPos > 100.0f) targetPos = 100.0f; + + // Hysterese in cm + if (_lastSentShadowPos >= 0.0f && s_crit < windowHeight && s_crit > 0.0f) + { + float delta_cm = fabsf(targetPos - _lastSentShadowPos) * windowHeight / 100.0f; + if (delta_cm < minShadowEdgeChange) + return; + } + + if (callContext.diagnosticLog) + logInfoP("Rollo geo position: gamma=%.1f° s_crit=%.1f targetPos=%.1f", gamma_rad * (180.0f / (float)M_PI), s_crit, targetPos); + + _lastSentShadowPos = targetPos; // ungeclippt für korrekte Hysterese + + float sendPos = targetPos; + if (ParamSHC_CShading1RolloPositionMode == 2) + { + uint8_t minClipPos = ParamSHC_CShading1MinClipPosition; + uint8_t maxClipPos = ParamSHC_CShading1MaxClipPosition; + if (minClipPos <= maxClipPos) + { + if (sendPos < (float)minClipPos) sendPos = (float)minClipPos; + if (sendPos > (float)maxClipPos) sendPos = (float)maxClipPos; + } + } + + positionController.setAutomaticPosition((uint8_t)sendPos); return; + } // Jalousie + if (!positionController.hasSlat()) + return; + switch (ParamSHC_CShading1SlatElevationDepending) { case 0: @@ -680,9 +888,9 @@ void ModeShading::control(const CallContext &callContext, PositionController &po } case 2: { - // Benutzerdefiniert: linear interpolation between posLow (sun low) and posHigh (sun high) + // Lamellennachführung Min/Max: linear interpolation between posLow (sun low) and posHigh (sun high) if (callContext.diagnosticLog) - logInfoP("ModeShading control Benutzerdefiniert"); + logInfoP("ModeShading control Lamellennachführung Min/Max"); double elevMin = (double)ParamSHC_CShading1ElevationMin; double elevMax = (double)ParamSHC_CShading1ElevationMax; double t = (elevMax > elevMin) ? (callContext.elevation - elevMin) / (elevMax - elevMin) : 0.5; @@ -697,9 +905,512 @@ void ModeShading::control(const CallContext &callContext, PositionController &po auto slatPosition = (uint8_t)targetSlatPosition; if (callContext.diagnosticLog) logInfoP("Calculated slat position %d for elevation %lf (t=%.2f, low=%d, high=%d)", (int)slatPosition, callContext.elevation, t, (int)posLow, (int)posHigh); + if (!callContext.modeNewStarted && abs((uint8_t)KoSHC_CShutterSlatOutput.value(DPT_Scaling) - slatPosition) < ParamSHC_CShading1MinChangeForSlatAdaption) + break; positionController.setAutomaticSlat(slatPosition); break; } + case 3: + { + // Geo. Positionsnachführung (Jalousie): Position + feste Lamellenstellung + if (ParamSHC_CType == 2) break; // Schutzguard + static const float DEG2RAD = (float)M_PI / 180.0f; + _notAllowedReason &= ~(ModeShadingNotAllowedReasonProfileAngleSentinel | ModeShadingNotAllowedReasonFlatRoofGuard); + + float gamma_rad; + float sin_alpha; + const uint8_t orientation = ParamSHC_CWindowOrientation; + const float facadeInclination = (float)(int8_t)ParamSHC_CFacadeInclination; + + if (orientation == 5) + { + // Dachfläche: gamma = Höhenwinkel, sin_alpha = |sin(Dachneigung)| + float elev = (float)callContext.elevation; + if (elev <= 0.0f) + return; + sin_alpha = fabsf(sinf(facadeInclination * DEG2RAD)); + if (sin_alpha < 0.05f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonFlatRoofGuard; + return; + } + gamma_rad = elev * DEG2RAD; + } + else + { + float beta_deg = calculateProfileAngle( + (float)callContext.elevation, + (float)callContext.azimuth, + orientation, + facadeInclination); + if (beta_deg <= 0.0f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + return; + } + gamma_rad = beta_deg * DEG2RAD; + sin_alpha = 1.0f; + } + + float windowHeight = (float)ParamSHC_CShading1WindowHeight; + float maxPenetration = (float)ParamSHC_CShading1MaxPenetrationDepth; + float windowSillHeight = (float)ParamSHC_CShading1WindowSillHeight; + float minShadowEdgeChange = (float)ParamSHC_CShading1MinShadowEdgeChange; + + float s_crit = (maxPenetration * tanf(gamma_rad) - windowSillHeight) / sin_alpha; + + float targetPos; + if (s_crit <= 0.0f) + targetPos = 100.0f; + else if (s_crit >= windowHeight) + targetPos = (float)ParamSHC_CShading1ShadingPosition; + else + targetPos = (1.0f - s_crit / windowHeight) * 100.0f; + + if (targetPos < 0.0f) targetPos = 0.0f; + if (targetPos > 100.0f) targetPos = 100.0f; + + if (_lastSentShadowPos >= 0.0f && s_crit < windowHeight && s_crit > 0.0f) + { + float delta_cm = fabsf(targetPos - _lastSentShadowPos) * windowHeight / 100.0f; + if (delta_cm < minShadowEdgeChange) + { + positionController.setAutomaticSlat(ParamSHC_CShading1SlatShadingPosition); + return; + } + } + + if (callContext.diagnosticLog) + logInfoP("Case3 geo position: gamma=%.1f° s_crit=%.1f targetPos=%.1f", gamma_rad * (180.0f / (float)M_PI), s_crit, targetPos); + + _lastSentShadowPos = targetPos; + positionController.setAutomaticPosition((uint8_t)targetPos); + positionController.setAutomaticSlat(ParamSHC_CShading1SlatShadingPosition); + break; + } + case 4: + { + // Geo. Positionsnachführung (Min/Max) (Jalousie): wie case 3 + Position-Clipping + if (ParamSHC_CType == 2) break; // Schutzguard + static const float DEG2RAD = (float)M_PI / 180.0f; + _notAllowedReason &= ~(ModeShadingNotAllowedReasonProfileAngleSentinel | ModeShadingNotAllowedReasonFlatRoofGuard); + + float gamma_rad; + float sin_alpha; + const uint8_t orientation = ParamSHC_CWindowOrientation; + const float facadeInclination = (float)(int8_t)ParamSHC_CFacadeInclination; + + if (orientation == 5) + { + float elev = (float)callContext.elevation; + if (elev <= 0.0f) + return; + sin_alpha = fabsf(sinf(facadeInclination * DEG2RAD)); + if (sin_alpha < 0.05f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonFlatRoofGuard; + return; + } + gamma_rad = elev * DEG2RAD; + } + else + { + float beta_deg = calculateProfileAngle( + (float)callContext.elevation, + (float)callContext.azimuth, + orientation, + facadeInclination); + if (beta_deg <= 0.0f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + return; + } + gamma_rad = beta_deg * DEG2RAD; + sin_alpha = 1.0f; + } + + float windowHeight = (float)ParamSHC_CShading1WindowHeight; + float maxPenetration = (float)ParamSHC_CShading1MaxPenetrationDepth; + float windowSillHeight = (float)ParamSHC_CShading1WindowSillHeight; + float minShadowEdgeChange = (float)ParamSHC_CShading1MinShadowEdgeChange; + + float s_crit = (maxPenetration * tanf(gamma_rad) - windowSillHeight) / sin_alpha; + + float targetPos; + if (s_crit <= 0.0f) + targetPos = 100.0f; + else if (s_crit >= windowHeight) + targetPos = (float)ParamSHC_CShading1ShadingPosition; + else + targetPos = (1.0f - s_crit / windowHeight) * 100.0f; + + if (targetPos < 0.0f) targetPos = 0.0f; + if (targetPos > 100.0f) targetPos = 100.0f; + + if (_lastSentShadowPos >= 0.0f && s_crit < windowHeight && s_crit > 0.0f) + { + float delta_cm = fabsf(targetPos - _lastSentShadowPos) * windowHeight / 100.0f; + if (delta_cm < minShadowEdgeChange) + { + positionController.setAutomaticSlat(ParamSHC_CShading1SlatShadingPosition); + return; + } + } + + _lastSentShadowPos = targetPos; // ungeclippt für korrekte Hysterese + + float sendPos = targetPos; + uint8_t minClipPos = ParamSHC_CShading1MinClipPosition; + uint8_t maxClipPos = ParamSHC_CShading1MaxClipPosition; + if (minClipPos <= maxClipPos) + { + if (sendPos < (float)minClipPos) sendPos = (float)minClipPos; + if (sendPos > (float)maxClipPos) sendPos = (float)maxClipPos; + } + + if (callContext.diagnosticLog) + logInfoP("Case4 geo pos min/max: gamma=%.1f° s_crit=%.1f targetPos=%.1f sendPos=%.1f", gamma_rad * (180.0f / (float)M_PI), s_crit, targetPos, sendPos); + + positionController.setAutomaticPosition((uint8_t)sendPos); + positionController.setAutomaticSlat(ParamSHC_CShading1SlatShadingPosition); + break; + } + case 5: + { + // Schattenkanten- und Lamellennachführung: Position (Case 4) + Lamellenwinkel (Case 3) + if (ParamSHC_CType == 2) break; // Schutzguard + static const float DEG2RAD = (float)M_PI / 180.0f; + _notAllowedReason &= ~(ModeShadingNotAllowedReasonProfileAngleSentinel | ModeShadingNotAllowedReasonFlatRoofGuard); + + float gamma_rad; + float sin_alpha; + bool slatTrackingPossible; + const uint8_t orientation = ParamSHC_CWindowOrientation; + const float facadeInclination = (float)(int8_t)ParamSHC_CFacadeInclination; + + if (orientation == 5) + { + // Dachfläche: gamma = Höhenwinkel, sin_alpha = |sin(Dachneigung)| + float elev = (float)callContext.elevation; + if (elev <= 0.0f) + return; + sin_alpha = fabsf(sinf(facadeInclination * DEG2RAD)); + if (sin_alpha < 0.05f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonFlatRoofGuard; + return; // Flachdach — kein sinnvolles Positionstracking + } + gamma_rad = elev * DEG2RAD; + // Für Dachfläche: calculateProfileAngle() prüfen ob Lamellennachführung möglich + float beta_slat = (float)callContext.elevation - facadeInclination; + slatTrackingPossible = (beta_slat > 0.0f); + if (!slatTrackingPossible) + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + } + else + { + float beta_deg = calculateProfileAngle( + (float)callContext.elevation, + (float)callContext.azimuth, + orientation, + facadeInclination); + if (beta_deg <= 0.0f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + return; + } + gamma_rad = beta_deg * DEG2RAD; + sin_alpha = 1.0f; // senkrechte Fassade: sin(90°) = 1 + slatTrackingPossible = true; + } + + float windowHeight = (float)ParamSHC_CShading1WindowHeight; + float maxPenetration = (float)ParamSHC_CShading1MaxPenetrationDepth; + float windowSillHeight = (float)ParamSHC_CShading1WindowSillHeight; + float minShadowEdgeChange = (float)ParamSHC_CShading1MinShadowEdgeChange; + + float s_crit = (maxPenetration * tanf(gamma_rad) - windowSillHeight) / sin_alpha; + + float targetPos; + if (s_crit <= 0.0f) + targetPos = 100.0f; + else if (s_crit >= windowHeight) + targetPos = (float)ParamSHC_CShading1ShadingPosition; + else + targetPos = (1.0f - s_crit / windowHeight) * 100.0f; + + if (targetPos < 0.0f) targetPos = 0.0f; + if (targetPos > 100.0f) targetPos = 100.0f; + + bool positionChanged = true; + if (_lastSentShadowPos >= 0.0f && s_crit < windowHeight && s_crit > 0.0f) + { + float delta_cm = fabsf(targetPos - _lastSentShadowPos) * windowHeight / 100.0f; + if (delta_cm < minShadowEdgeChange) + positionChanged = false; + } + + if (positionChanged) + { + _lastSentShadowPos = targetPos; + positionController.setAutomaticPosition((uint8_t)targetPos); + } + + // Lamellenwinkel — Betriebsart: 0=Tageslicht, 1=Blendschutz, 2=Tabelle + uint8_t betriebsart = ParamSHC_CShading1SlatTrackingBetriebsart; + if (slatTrackingPossible) + { + float gamma_deg = gamma_rad * (180.0f / (float)M_PI); + if (betriebsart == 2) + { + float startPos = (float)ParamSHC_CShading1SlatTableStartPos; + float minElevation = (float)ParamSHC_CShading1SlatTableMinElevation; + float e1 = (float)ParamSHC_CShading1SlatTable1Elevation; + float p1 = (float)ParamSHC_CShading1SlatTable1Position; + float e2 = (float)ParamSHC_CShading1SlatTable2Elevation; + float p2 = (float)ParamSHC_CShading1SlatTable2Position; + float e3 = (float)ParamSHC_CShading1SlatTable3Elevation; + float p3 = (float)ParamSHC_CShading1SlatTable3Position; + float e4 = (float)ParamSHC_CShading1SlatTable4Elevation; + float p4 = (float)ParamSHC_CShading1SlatTable4Position; + float e5 = (float)ParamSHC_CShading1SlatTable5Elevation; + float p5 = (float)ParamSHC_CShading1SlatTable5Position; + float e6 = (float)ParamSHC_CShading1SlatTable6Elevation; + float p6 = (float)ParamSHC_CShading1SlatTable6Position; + float slatPercent; + if (gamma_deg <= minElevation) + slatPercent = startPos; + else if (gamma_deg >= e6) + slatPercent = p6; + else + { + float lo_e, lo_p, hi_e, hi_p; + if (gamma_deg < e2) { lo_e = e1; lo_p = p1; hi_e = e2; hi_p = p2; } + else if (gamma_deg < e3) { lo_e = e2; lo_p = p2; hi_e = e3; hi_p = p3; } + else if (gamma_deg < e4) { lo_e = e3; lo_p = p3; hi_e = e4; hi_p = p4; } + else if (gamma_deg < e5) { lo_e = e4; lo_p = p4; hi_e = e5; hi_p = p5; } + else { lo_e = e5; lo_p = p5; hi_e = e6; hi_p = p6; } + slatPercent = (hi_e > lo_e) ? lo_p + (hi_p - lo_p) * (gamma_deg - lo_e) / (hi_e - lo_e) : lo_p; + } + slatPercent += (float)(int8_t)ParamSHC_CShading1OffsetSlatPosition; + if (slatPercent < 0.0f) slatPercent = 0.0f; + if (slatPercent > 100.0f) slatPercent = 100.0f; + auto slatPosition = (uint8_t)slatPercent; + if (callContext.modeNewStarted || abs((uint8_t)KoSHC_CShutterSlatOutput.value(DPT_Scaling) - slatPosition) >= ParamSHC_CShading1MinChangeForSlatAdaption) + positionController.setAutomaticSlat(slatPosition); + } + else + { + float a = (float)ParamSHC_CShading1SlatSpacing; + float b = (float)ParamSHC_CShading1SlatWidth; + uint8_t angleAtMin = ParamSHC_CShading1SlatAngleAtMin; + uint8_t angleAtMax = ParamSHC_CShading1SlatAngleAtMax; + + if (angleAtMin != angleAtMax) + { + float theta_krit = (betriebsart == 1) + ? gamma_deg + : atan2f(a * sinf(gamma_rad), b - a * cosf(gamma_rad)) * (180.0f / (float)M_PI); + + if (theta_krit < 0.0f) + { + positionController.setAutomaticSlat(100); + } + else + { + float slatPercent = ((float)theta_krit - (float)angleAtMin) / ((float)angleAtMax - (float)angleAtMin) * 100.0f; + slatPercent += (float)(int8_t)ParamSHC_CShading1OffsetSlatPosition; + if (slatPercent < 0.0f) slatPercent = 0.0f; + if (slatPercent > 100.0f) slatPercent = 100.0f; + auto slatPosition = (uint8_t)slatPercent; + + if (callContext.modeNewStarted || abs((uint8_t)KoSHC_CShutterSlatOutput.value(DPT_Scaling) - slatPosition) >= ParamSHC_CShading1MinChangeForSlatAdaption) + positionController.setAutomaticSlat(slatPosition); + } + } + } + } + + if (callContext.diagnosticLog) + logInfoP("Case5 combined: gamma=%.1f° s_crit=%.1f targetPos=%.1f slatPossible=%d betriebsart=%d", gamma_rad * (180.0f / (float)M_PI), s_crit, targetPos, (int)slatTrackingPossible, (int)betriebsart); + + break; + } + case 6: + { + // Geo. Positions- und Lamellennachführung mit Begrenzung + // Identisch zu Case 5, jedoch mit konfigurierbarem Clipping für Position und Lamelle + if (ParamSHC_CType == 2) break; + static const float DEG2RAD = (float)M_PI / 180.0f; + _notAllowedReason &= ~(ModeShadingNotAllowedReasonProfileAngleSentinel | ModeShadingNotAllowedReasonFlatRoofGuard); + + float gamma_rad; + float sin_alpha; + bool slatTrackingPossible; + const uint8_t orientation = ParamSHC_CWindowOrientation; + const float facadeInclination = (float)(int8_t)ParamSHC_CFacadeInclination; + + if (orientation == 5) + { + float elev = (float)callContext.elevation; + if (elev <= 0.0f) + return; + sin_alpha = fabsf(sinf(facadeInclination * DEG2RAD)); + if (sin_alpha < 0.05f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonFlatRoofGuard; + return; + } + gamma_rad = elev * DEG2RAD; + float beta_slat = (float)callContext.elevation - facadeInclination; + slatTrackingPossible = (beta_slat > 0.0f); + if (!slatTrackingPossible) + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + } + else + { + float beta_deg = calculateProfileAngle( + (float)callContext.elevation, + (float)callContext.azimuth, + orientation, + facadeInclination); + if (beta_deg <= 0.0f) + { + _notAllowedReason |= ModeShadingNotAllowedReasonProfileAngleSentinel; + return; + } + gamma_rad = beta_deg * DEG2RAD; + sin_alpha = 1.0f; + slatTrackingPossible = true; + } + + float windowHeight = (float)ParamSHC_CShading1WindowHeight; + float maxPenetration = (float)ParamSHC_CShading1MaxPenetrationDepth; + float windowSillHeight = (float)ParamSHC_CShading1WindowSillHeight; + float minShadowEdgeChange = (float)ParamSHC_CShading1MinShadowEdgeChange; + + float s_crit = (maxPenetration * tanf(gamma_rad) - windowSillHeight) / sin_alpha; + + float targetPos; + if (s_crit <= 0.0f) + targetPos = 100.0f; + else if (s_crit >= windowHeight) + targetPos = (float)ParamSHC_CShading1ShadingPosition; + else + targetPos = (1.0f - s_crit / windowHeight) * 100.0f; + + if (targetPos < 0.0f) targetPos = 0.0f; + if (targetPos > 100.0f) targetPos = 100.0f; + + bool positionChanged = true; + if (_lastSentShadowPos >= 0.0f && s_crit < windowHeight && s_crit > 0.0f) + { + float delta_cm = fabsf(targetPos - _lastSentShadowPos) * windowHeight / 100.0f; + if (delta_cm < minShadowEdgeChange) + positionChanged = false; + } + + if (positionChanged) + { + _lastSentShadowPos = targetPos; // Ungeclippten Wert für korrekte Hysterese speichern + float clippedPos = targetPos; + uint8_t minClipPos = ParamSHC_CShading1MinClipPosition; + uint8_t maxClipPos = ParamSHC_CShading1MaxClipPosition; + if (minClipPos <= maxClipPos) + { + if (clippedPos < (float)minClipPos) clippedPos = (float)minClipPos; + if (clippedPos > (float)maxClipPos) clippedPos = (float)maxClipPos; + } + positionController.setAutomaticPosition((uint8_t)clippedPos); + } + + uint8_t betriebsart = ParamSHC_CShading1SlatTrackingBetriebsart; + if (slatTrackingPossible) + { + float gamma_deg = gamma_rad * (180.0f / (float)M_PI); + if (betriebsart == 2) + { + float startPos = (float)ParamSHC_CShading1SlatTableStartPos; + float minElevation = (float)ParamSHC_CShading1SlatTableMinElevation; + float e1 = (float)ParamSHC_CShading1SlatTable1Elevation; + float p1 = (float)ParamSHC_CShading1SlatTable1Position; + float e2 = (float)ParamSHC_CShading1SlatTable2Elevation; + float p2 = (float)ParamSHC_CShading1SlatTable2Position; + float e3 = (float)ParamSHC_CShading1SlatTable3Elevation; + float p3 = (float)ParamSHC_CShading1SlatTable3Position; + float e4 = (float)ParamSHC_CShading1SlatTable4Elevation; + float p4 = (float)ParamSHC_CShading1SlatTable4Position; + float e5 = (float)ParamSHC_CShading1SlatTable5Elevation; + float p5 = (float)ParamSHC_CShading1SlatTable5Position; + float e6 = (float)ParamSHC_CShading1SlatTable6Elevation; + float p6 = (float)ParamSHC_CShading1SlatTable6Position; + float slatPercent; + if (gamma_deg <= minElevation) + slatPercent = startPos; + else if (gamma_deg >= e6) + slatPercent = p6; + else + { + float lo_e, lo_p, hi_e, hi_p; + if (gamma_deg < e2) { lo_e = e1; lo_p = p1; hi_e = e2; hi_p = p2; } + else if (gamma_deg < e3) { lo_e = e2; lo_p = p2; hi_e = e3; hi_p = p3; } + else if (gamma_deg < e4) { lo_e = e3; lo_p = p3; hi_e = e4; hi_p = p4; } + else if (gamma_deg < e5) { lo_e = e4; lo_p = p4; hi_e = e5; hi_p = p5; } + else { lo_e = e5; lo_p = p5; hi_e = e6; hi_p = p6; } + slatPercent = (hi_e > lo_e) ? lo_p + (hi_p - lo_p) * (gamma_deg - lo_e) / (hi_e - lo_e) : lo_p; + } + slatPercent += (float)(int8_t)ParamSHC_CShading1OffsetSlatPosition; + if (slatPercent < 0.0f) slatPercent = 0.0f; + if (slatPercent > 100.0f) slatPercent = 100.0f; + auto slatPosition = (uint8_t)slatPercent; + if (callContext.modeNewStarted || abs((uint8_t)KoSHC_CShutterSlatOutput.value(DPT_Scaling) - slatPosition) >= ParamSHC_CShading1MinChangeForSlatAdaption) + positionController.setAutomaticSlat(slatPosition); + } + else + { + float a = (float)ParamSHC_CShading1SlatSpacing; + float b = (float)ParamSHC_CShading1SlatWidth; + uint8_t angleAtMin = ParamSHC_CShading1SlatAngleAtMin; + uint8_t angleAtMax = ParamSHC_CShading1SlatAngleAtMax; + + if (angleAtMin != angleAtMax) + { + float theta_krit = (betriebsart == 1) + ? gamma_deg + : atan2f(a * sinf(gamma_rad), b - a * cosf(gamma_rad)) * (180.0f / (float)M_PI); + + if (theta_krit < 0.0f) + { + positionController.setAutomaticSlat(100); + } + else + { + float slatPercent = ((float)theta_krit - (float)angleAtMin) / ((float)angleAtMax - (float)angleAtMin) * 100.0f; + slatPercent += (float)(int8_t)ParamSHC_CShading1OffsetSlatPosition; + if (slatPercent < 0.0f) slatPercent = 0.0f; + if (slatPercent > 100.0f) slatPercent = 100.0f; + + // Lamellen-Clipping: min(PPP+51, PPP+52) bis max(PPP+51, PPP+52) + float slatLow = (float)ParamSHC_CShading1SlatLowSunPosition; + float slatHigh = (float)ParamSHC_CShading1SlatHighSunPosition; + float slatClipMin = (slatLow < slatHigh) ? slatLow : slatHigh; + float slatClipMax = (slatLow > slatHigh) ? slatLow : slatHigh; + if (slatPercent < slatClipMin) slatPercent = slatClipMin; + if (slatPercent > slatClipMax) slatPercent = slatClipMax; + + auto slatPosition = (uint8_t)slatPercent; + if (callContext.modeNewStarted || abs((uint8_t)KoSHC_CShutterSlatOutput.value(DPT_Scaling) - slatPosition) >= ParamSHC_CShading1MinChangeForSlatAdaption) + positionController.setAutomaticSlat(slatPosition); + } + } + } + } + + if (callContext.diagnosticLog) + logInfoP("Case6 clip: gamma=%.1f° s_crit=%.1f targetPos=%.1f slatPossible=%d betriebsart=%d", gamma_rad * (180.0f / (float)M_PI), s_crit, targetPos, (int)slatTrackingPossible, (int)betriebsart); + + break; + } } } @@ -745,6 +1456,11 @@ void ModeShading::processInputKo(GroupObject &ko, PositionController &positionCo _recalcMeasurmentValues = true; } +bool ModeShading::isPositionAllowed(const CallContext& callContext) const +{ + return callContext.positionController->targetPosition() <= ParamSHC_CShading1OnlyIfLessThan; +} + bool ModeShading::isModeShading() const { return true; diff --git a/src/ModeShading.h b/src/ModeShading.h index 0917411..c43b984 100644 --- a/src/ModeShading.h +++ b/src/ModeShading.h @@ -29,6 +29,8 @@ enum ModeShadingNotAllowedReason : uint32_t ModeShadingNotAllowedReasonTemperatureForecase = 1048576, ModeShadingNotAllowedReasonClouds = 2097152, ModeShadingNotAllowedReasonUVI = 4194304, + ModeShadingNotAllowedReasonProfileAngleSentinel = 8388608, // Bit 23: Profilwinkel nicht berechenbar + ModeShadingNotAllowedReasonFlatRoofGuard = 16777216, // Bit 24: Flachdach-Schutz aktiv }; @@ -50,9 +52,11 @@ class ModeShading : public ModeBase bool _needWaitTime = false; unsigned long _lastHeadingTimeStamp = 0; bool _heatingOff = true; + float _lastSentShadowPos = -1.0f; bool allowedByMeasurmentValues(const CallContext& callContext); 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(); + float calculateProfileAngle(float elevationDeg, float azimuthDeg, uint8_t orientationEnum, float facadeInclinationDeg) const; public: ModeShading(uint8_t index); bool allowedBySun(const CallContext& callContext); @@ -68,4 +72,6 @@ class ModeShading : public ModeBase void stop(const CallContext& callContext, const ModeBase* next, PositionController& positionController) override; void processInputKo(GroupObject &ko, PositionController& positionController) override; bool isModeShading() const override; +public: + bool isPositionAllowed(const CallContext& callContext) const; }; \ No newline at end of file diff --git a/src/ShutterControllerChannel.cpp b/src/ShutterControllerChannel.cpp index cbec2d3..9ec8b18 100755 --- a/src/ShutterControllerChannel.cpp +++ b/src/ShutterControllerChannel.cpp @@ -80,6 +80,7 @@ void ShutterControllerChannel::setup() KoSHC_CShadingControl.value(shadingControlActive(), DPT_Switch); KoSHC_CShadingControlActive.value(shadingControlActive(), DPT_Switch); KoSHC_CShadingActive.value(false, DPT_Switch); + KoSHC_CShadingReadyUser.value(false, DPT_Switch); if (KoSHC_CLock.initialized()) { @@ -595,6 +596,7 @@ void ShutterControllerChannel::execute(CallContext &callContext) _currentWindowOpenHandler->start(callContext, _currentWindowOpenHandler, _positionController); } callContext.isWindowOpenActive = _currentWindowOpenHandler != nullptr; + callContext.isWindowTiltActive = _currentWindowOpenHandler != nullptr && _currentWindowOpenHandler->isTiltHandler(); // State machine handling for mode activation ModeBase *nextMode = nullptr; @@ -711,12 +713,29 @@ void ShutterControllerChannel::execute(CallContext &callContext) KoSHC_CShading2LockActive.value(DPT_Switch) || KoSHC_CShading2BreakLockActive.value(DPT_Switch); } + bool shadingPositionAllowed = _currentMode != nullptr && _currentMode->isModeShading(); + if (!shadingPositionAllowed) + { + for (auto mode : _modes) + { + if (mode->isModeShading()) + { + auto modeShading = (ModeShading *)mode; + if (modeShading->isPositionAllowed(callContext)) + { + shadingPositionAllowed = true; + break; + } + } + } + } bool readinessUser = shadingControlActive() && !_channelLockActive && _currentWindowOpenHandler == nullptr && _windowOpenState == WindowOpenStateClosed && _currentMode != _modeManual && - !shadingModeLockActive; + !shadingModeLockActive && + shadingPositionAllowed; if (KoSHC_CShadingReadyUser.valueNoSendCompare(readinessUser, DPT_Switch)) KoSHC_CShadingReadyUser.objectWritten(); @@ -732,6 +751,15 @@ void ShutterControllerChannel::execute(CallContext &callContext) logInfoP("Shading active: %d", anyShadingModeActive()); logInfoP("Active shading period: %d", _shadingPeriodActive); logInfoP("Current active mode: %s", _currentMode->name()); + logInfoP("ReadyUser: %d (shadCtrl=%d lock=%d winHandler=%d winState=%d manual=%d modeLock=%d posAllowed=%d)", + readinessUser, + (int)shadingControlActive(), + (int)_channelLockActive, + (int)(_currentWindowOpenHandler != nullptr), + (int)_windowOpenState, + (int)(_currentMode == _modeManual), + (int)shadingModeLockActive, + (int)shadingPositionAllowed); } } diff --git a/src/ShutterControllerModule.ModeShading.xml b/src/ShutterControllerModule.ModeShading.xml index ed4460a..74dabc5 100755 --- a/src/ShutterControllerModule.ModeShading.xml +++ b/src/ShutterControllerModule.ModeShading.xml @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + @@ -33,7 +33,7 @@ - + @@ -52,8 +52,7 @@ - - + @@ -84,9 +83,13 @@ - + + + + + @@ -151,10 +154,85 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -267,7 +345,57 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -334,14 +462,29 @@ - - - - + + + + + + + + + + + + + + + + + + + @@ -460,9 +603,6 @@ - - - @@ -470,6 +610,8 @@ + + @@ -489,9 +631,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -662,7 +980,17 @@ - + + + + + + + + + + + diff --git a/src/ShutterControllerModule.ModeWindowOpen.xml b/src/ShutterControllerModule.ModeWindowOpen.xml index 9753f4c..230290b 100755 --- a/src/ShutterControllerModule.ModeWindowOpen.xml +++ b/src/ShutterControllerModule.ModeWindowOpen.xml @@ -138,8 +138,13 @@ - - + + + + + + + @@ -151,16 +156,26 @@ - - - - + + + + + + + + + - - + + + + + + + @@ -172,10 +187,15 @@ - - - - + + + + + + + + + @@ -266,6 +286,23 @@ + + + + + + + + + + + + + + + + + diff --git a/src/ShutterControllerModule.share.xml b/src/ShutterControllerModule.share.xml index d82edb4..94ee272 100755 --- a/src/ShutterControllerModule.share.xml +++ b/src/ShutterControllerModule.share.xml @@ -1,4 +1,4 @@ - + @@ -35,10 +35,10 @@ - + - + @@ -194,12 +194,60 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ShutterControllerModule.templ.xml b/src/ShutterControllerModule.templ.xml index 50671e4..ff5571e 100755 --- a/src/ShutterControllerModule.templ.xml +++ b/src/ShutterControllerModule.templ.xml @@ -8,7 +8,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -49,8 +49,20 @@ - - + + + + + + + + + + + + + + @@ -202,9 +214,11 @@ - - + + + + @@ -221,6 +235,18 @@ + + + + + + + + + + + + @@ -240,6 +266,8 @@ + + @@ -572,6 +600,9 @@ + + + @@ -583,16 +614,28 @@ + + + + + + + + + + + + @@ -635,6 +678,8 @@ + + @@ -646,14 +691,29 @@ + + + + + + + + + + + + + + + @@ -762,10 +822,15 @@ + + + + + @@ -780,6 +845,9 @@ + + + @@ -791,6 +859,8 @@ + + @@ -905,6 +975,10 @@ + + + + @@ -1034,7 +1108,18 @@ - + + + + + + + + + + + + @@ -1046,4 +1131,4 @@ - \ No newline at end of file + diff --git a/src/WindowOpenHandler.h b/src/WindowOpenHandler.h index 0e58040..721945e 100644 --- a/src/WindowOpenHandler.h +++ b/src/WindowOpenHandler.h @@ -26,6 +26,7 @@ class WindowOpenHandler protected: const std::string& logPrefix() const; public: + bool isTiltHandler() const { return _isTiltHandler; } uint8_t getParamterOpenPositionControl(); uint8_t getParamterOpenSlatPositionControl(); uint8_t getParamterOpenPosition(); diff --git a/test/README.md b/test/README.md deleted file mode 100644 index 00984c0..0000000 --- a/test/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# 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 deleted file mode 100644 index 1cb89aa..0000000 --- a/test/brightness_logic_test.py +++ /dev/null @@ -1,149 +0,0 @@ -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()