diff --git a/Climate/bosch_bth-ra_control.yaml b/Climate/bosch_bth-ra_control.yaml index 2f2dcdd..0a31687 100644 --- a/Climate/bosch_bth-ra_control.yaml +++ b/Climate/bosch_bth-ra_control.yaml @@ -6,6 +6,11 @@ blueprint: min_version: "2024.6.0" description: > Eine Automation zur automatischen Steuerung eines Bosch TRV Heizkörperthermostats. + + **Temperatur-Prioritätslogik:** + 1. Alarm armed_away → Abwesenheitstemperatur + 2. Aktiver Zeitplan → Temperatur aus Schedule + 3. Fallback → Abwesenheitstemperatur domain: automation author: Me input: @@ -25,6 +30,24 @@ blueprint: filter: - domain: sensor device_class: temperature + sensor_sync_threshold: + name: Sensor-Synchronisations-Schwellenwert + description: > + Minimale Temperaturdifferenz in °C, um eine Sensor-Synchronisation auszulösen (Default = 0.5°C). + + **Synchronisation erfolgt bei:** + 1. Differenz zwischen Sensor und TRV ≥ Schwellenwert + 2. Sensor kreuzt Solltemperatur von unten (verhindert zu spätes Aufheizen) + 3. Sensor kreuzt Solltemperatur von oben (verhindert Überhitzung) + 4. Spätestens nach 25 Minuten ohne Änderung + default: 0.5 + selector: + number: + mode: box + min: 0.1 + max: 1.0 + unit_of_measurement: "°C" + step: 0.1 heating_period_switch: name: Heizperiode Switch description: Optional - Input Boolean der angibt ob Heizperiode aktiv ist. Wenn nicht gesetzt, ist die Heizperiode immer aktiv. @@ -35,7 +58,9 @@ blueprint: - domain: input_boolean min_temperature: name: Minimale Temperatur - description: Minimale erlaubte Solltemperatur als Sicherheitsgrenze (Default = 15°C) + description: > + Minimale erlaubte Solltemperatur als Sicherheitsgrenze (Default = 15°C). + Alle berechneten Temperaturen werden auf diesen Minimalwert begrenzt (Clamping). default: 15 selector: number: @@ -46,7 +71,9 @@ blueprint: step: 0.5 max_temperature: name: Maximale Temperatur - description: Maximale erlaubte Solltemperatur als Sicherheitsgrenze (Default = 28°C) + description: > + Maximale erlaubte Solltemperatur als Sicherheitsgrenze (Default = 23°C). + Alle berechneten Temperaturen werden auf diesen Maximalwert begrenzt (Clamping). default: 23 selector: number: @@ -58,7 +85,7 @@ blueprint: window_section: name: Fenster-/Türsensor Konfiguration description: Konfiguration für den Fenster-/Türsensor. - collapsed: false + collapsed: true input: window_sensor: name: Fenster-/Türsensor (oder Gruppe) @@ -92,7 +119,7 @@ blueprint: schedule_section: name: Heizplan Konfiguration description: Konfiguration der Heizpläne die für den Radiator berücksichtigt werden sollen - collapsed: false + collapsed: true input: radiator_schedules: name: Heizpläne @@ -123,7 +150,7 @@ blueprint: away_section: name: Konfiguration für Abwesenheitsmodus description: Konfiguration für das Absenken der Heizung im Abwesenheitsmodus eines Alarmsystems - collapsed: false + collapsed: true input: alarm_control_panel: name: Alarm Control Panel @@ -135,7 +162,12 @@ blueprint: - domain: alarm_control_panel away_temperature: name: Abwesenheitstemperatur - description: Temperatur die eingestellt wird, wenn der Alarm im Abwesendmodus ist (Default = 18°C) + description: > + Temperatur die verwendet wird, wenn der Alarm im Abwesendmodus ist oder kein Zeitplan aktiv ist (Default = 18°C). + + **Verwendung:** + - Primär: Alarm Control Panel im Modus 'armed_away' (höchste Priorität) + - Fallback: Wenn kein aktiver Zeitplan eine Temperatur liefert default: 18 selector: number: @@ -144,10 +176,39 @@ blueprint: max: 25.0 unit_of_measurement: "°C" step: 0.5 + override_section: + name: Override Reset Konfiguration + description: Automatisches Zurücksetzen der Solltemperatur nach manueller Änderung am Thermostat + collapsed: true + input: + override_reset_duration: + name: Override Reset Zeitraum + description: > + Zeit nach der eine manuelle Temperaturänderung automatisch auf den Heizplan zurückgesetzt wird. + + **Feature deaktiviert wenn auf 00:00:00 gesetzt** (Default = 00:00:00) + + **Verhalten:** + - Nur manuelle Änderungen am Thermostat werden zurückgesetzt (nicht über Home Assistant) + - Timer startet neu bei jeder weiteren manuellen Änderung + - Reset erfolgt nur wenn Solltemperatur vom Heizplan abweicht + - Wenn kein Schedule aktiv ist, wird auf Abwesenheitstemperatur zurückgesetzt + - Alarm-Modus hat Priorität: Bei aktivem Alarm erfolgt kein Reset (Abwesenheitstemperatur gilt) + + Beispiel: Nach 2 Stunden wird die Temperatur wieder auf den aktiven Heizplan-Wert gesetzt. + default: + hours: 0 + minutes: 0 + seconds: 0 + selector: + duration: + enable_day: false variables: + # Input-Variablen trv: !input trv temperature_sensor: !input temperature_sensor + sensor_sync_threshold: !input sensor_sync_threshold heating_period_switch: !input heating_period_switch radiator_schedules: !input radiator_schedules active_scheduler_selector: !input active_scheduler_selector @@ -155,101 +216,109 @@ variables: min_temperature: !input min_temperature max_temperature: !input max_temperature alarm_control_panel: !input alarm_control_panel - is_heating_period: > - {% if heating_period_switch is none or heating_period_switch == '' %} - true - {% else %} - {{ is_state(heating_period_switch, 'on') }} - {% endif %} + override_reset_duration: !input override_reset_duration + + # Konstanten + temperature_change_tolerance: 0.4 # °C - Minimale Differenz für Temperaturänderung + sensor_sync_max_age: 1499 # Sekunden (25 Minuten - 1 Sekunde) - Max Alter für Sensor-Sync + + # Entity-Discovery remote_temperature_entity: > - {% set entities = device_entities(device_id(trv)) %} - {% set remote_temperature_entity_id = namespace(id='') %} - {% for entity in entities %} - {% if 'remote_temperature' in entity %} - {% set remote_temperature_entity_id.id = entity %} - {% endif %} - {% endfor %} - {{ remote_temperature_entity_id.id }} + {{ device_entities(device_id(trv)) | select('search', 'remote_temperature') | list | first | default('') }} window_detection_entity: > - {% set entities = device_entities(device_id(trv)) %} - {% set window_detection_entity_id = namespace(id='') %} - {% for entity in entities %} - {% if 'window_detection' in entity %} - {% set window_detection_entity_id.id = entity %} - {% endif %} - {% endfor %} - {{ window_detection_entity_id.id }} + {{ device_entities(device_id(trv)) | select('search', 'window_detection') | list | first | default('') }} + setpoint_change_source_entity: > + {{ device_entities(device_id(trv)) | select('search', 'setpoint_change_source') | list | first | default('') }} + + # Entity-Validierung + remote_temperature_entity_valid: > + {{ remote_temperature_entity and states(remote_temperature_entity) not in ['unknown', 'unavailable', none] }} + window_detection_entity_valid: > + {{ window_detection_entity and window_detection_entity | length > 0 }} + setpoint_change_source_entity_valid: > + {{ setpoint_change_source_entity and states(setpoint_change_source_entity) not in ['unknown', 'unavailable', none] }} + + # Basis-Berechnungen + is_heating_period: > + {{ heating_period_switch in [none, ''] or is_state(heating_period_switch, 'on') }} + current_remote_temperature: > + {{ states(remote_temperature_entity) | float(0) if remote_temperature_entity_valid else 0 }} + remote_temperature_last_change: > + {{ states[remote_temperature_entity].last_changed if remote_temperature_entity_valid else none }} scheduled_temperature: > - {% set ns = namespace(current_temperature = none) %} - {% set selected_friendly_name = states(active_scheduler_selector) %} - {% if selected_friendly_name is not none and selected_friendly_name != 'unknown' %} - {% for schedule in radiator_schedules %} - {% if state_attr(schedule, 'friendly_name') == selected_friendly_name and states(schedule) == 'on' %} - {% set temp = state_attr(schedule, 'temp') %} - {% if temp is not none and temp | is_number %} - {% set ns.current_temperature = temp %} - {% break %} - {% endif %} + {% set selected = states(active_scheduler_selector) %} + {% if selected not in [none, 'unknown', ''] %} + {% for schedule in radiator_schedules if state_attr(schedule, 'friendly_name') == selected and is_state(schedule, 'on') %} + {% set temp = state_attr(schedule, 'temp') %} + {% if temp is not none and temp | is_number %} + {{ temp }} + {% break %} {% endif %} {% endfor %} {% endif %} - {{ ns.current_temperature }} + + # Temperatur-Berechnungen + # Priorität: 1. Alarm armed_away → away_temperature, 2. Aktiver Zeitplan → scheduled_temperature, 3. Fallback → away_temperature target_temperature: > {% if alarm_control_panel and is_state(alarm_control_panel, 'armed_away') %} {{ away_temperature }} - {% elif scheduled_temperature is not none %} + {% elif scheduled_temperature != none %} {{ scheduled_temperature }} {% else %} {{ away_temperature }} {% endif %} + # Begrenzt target_temperature auf den Bereich [min_temperature, max_temperature] (Clamping) safe_temperature: > - {% set temp = target_temperature | float(18) %} - {% if temp < min_temperature %} - {{ min_temperature }} - {% elif temp > max_temperature %} - {{ max_temperature }} - {% else %} - {{ temp }} - {% endif %} + {{ [min_temperature, [max_temperature, target_temperature | float(18)] | min] | max }} + + # Status-Prüfungen is_valid_temperature: > {{ safe_temperature is not none and safe_temperature | is_number }} is_temperature_change_needed: > - {% set current = state_attr(trv, 'temperature') | float(0) %} - {% set new = safe_temperature | float(0) %} - {% set diff = new - current %} - {% if new > current %} - true - {% elif diff | abs >= 0.4 %} - true - {% else %} - false - {% endif %} + {{ (safe_temperature | float(0) - state_attr(trv, 'temperature') | float(0)) | abs >= temperature_change_tolerance }} + # Synchronisation nötig wenn: 1) Differenz > Schwelle, 2) Sensor kreuzt Soll-Temp von unten, 3) Sensor kreuzt Soll-Temp von oben is_sensor_sync_needed: > - {% set current_remote_temp = states(remote_temperature_entity) | float(0) %} {% set new_sensor_temp = states(temperature_sensor) | float(0) %} {% set target_temp = state_attr(trv, 'temperature') | float(0) %} - {% set diff = (new_sensor_temp - current_remote_temp) | abs %} - {% if diff >= 0.5 %} - true - {% elif new_sensor_temp < target_temp and current_remote_temp >= target_temp %} - true - {% elif new_sensor_temp > target_temp and current_remote_temp <= target_temp %} - true + {% set diff = (new_sensor_temp - current_remote_temperature) | abs %} + {{ diff >= sensor_sync_threshold or + (new_sensor_temp < target_temp and current_remote_temperature >= target_temp) or + (new_sensor_temp > target_temp and current_remote_temperature <= target_temp) }} + scheduler_mismatch: > + {% set selected = states(active_scheduler_selector) %} + {% if selected not in ['unknown', '', none] %} + {% set schedule_found = namespace(value=false) %} + {% for schedule in radiator_schedules %} + {% if state_attr(schedule, 'friendly_name') == selected %} + {% set schedule_found.value = true %} + {% endif %} + {% endfor %} + {{ not schedule_found.value }} {% else %} false {% endif %} - scheduler_mismatch: > - {% set selected_friendly_name = states(active_scheduler_selector) %} - {% if selected_friendly_name not in ['unknown', '', none] %} - {% set found = namespace(value=false) %} - {% for schedule in radiator_schedules %} - {% if state_attr(schedule, 'friendly_name') == selected_friendly_name %} - {% set found.value = true %} - {% endif %} - {% endfor %} - {{ not found.value }} - {% else %} + is_manual_override: > + {{ (state_attr(trv, 'temperature') | float(0) - safe_temperature | float(0)) | abs >= temperature_change_tolerance }} + + # Override Reset Berechnungen + # Konvertiert duration-Dictionary {hours, minutes, seconds} in Gesamtsekunden + override_reset_duration_seconds: > + {{ (override_reset_duration.hours | default(0) | int) * 3600 + + (override_reset_duration.minutes | default(0) | int) * 60 + + (override_reset_duration.seconds | default(0) | int) if override_reset_duration is mapping else 0 }} + override_last_change: > + {{ states[setpoint_change_source_entity].last_updated if setpoint_change_source_entity_valid else none }} + is_setpoint_manual: > + {{ setpoint_change_source_entity_valid and states(setpoint_change_source_entity) == 'manual' }} + # Early exits: 1) Keine Änderung/Timeout=0 → false, 2) Nicht manuell → false, 3) Berechne Zeitdifferenz + override_duration_exceeded: > + {% if override_last_change == none or override_reset_duration_seconds == 0 %} false + {% elif not is_setpoint_manual %} + false + {% else %} + {% set last_change = as_datetime(override_last_change) %} + {{ last_change != none and (now() - last_change).total_seconds() > override_reset_duration_seconds }} {% endif %} triggers: @@ -259,21 +328,21 @@ triggers: from: "off" to: "on" for: !input window_delay_open - id: FENSTER_OPEN + id: WINDOW_OPENED - platform: state entity_id: - !input window_sensor from: "on" to: "off" for: !input window_delay_close - id: FENSTER_CLOSED + id: WINDOW_CLOSED - platform: time_pattern - # Synce Temperatur alle 5 Minuten (Teiler von 60) + # Periodische Überprüfung alle 5 Minuten (Temperatursynchronisation, Override-Check) minutes: "/5" - id: SYNC_TEMPERATURE + id: PERIODIC_CHECK - platform: state entity_id: !input temperature_sensor - id: TEMP_CHANGED + id: SENSOR_TEMPERATURE_CHANGED - platform: state entity_id: !input alarm_control_panel to: "armed_away" @@ -282,37 +351,43 @@ triggers: entity_id: !input alarm_control_panel from: "armed_away" to: "disarmed" - id: ALARM_DISARMED_AWAY + id: ALARM_DISARMED - platform: state entity_id: !input heating_period_switch to: "on" - id: HEATING_PERIOD_ON + id: HEATING_PERIOD_STARTED - platform: state entity_id: !input heating_period_switch to: "off" - id: HEATING_PERIOD_OFF + id: HEATING_PERIOD_ENDED - platform: state entity_id: !input radiator_schedules attribute: temp - id: SCHEDULE_TEMP_CHANGED + id: SCHEDULE_TEMPERATURE_CHANGED - platform: state entity_id: !input active_scheduler_selector - id: SCHEDULER_CHANGED + id: SCHEDULE_SELECTOR_CHANGED +# Hinweis zur Wartbarkeit: climate.set_temperature wird an 4 Stellen verwendet: +# 1. Heizperiode aktiviert - Setzt Temperatur beim Einschalten der Heizung +# 2. Fenster geschlossen - Setzt Temperatur nach Fensterschließung +# 3. Override Reset - Setzt Temperatur zurück nach manuellem Override +# 4. Schedule/Alarm-Änderungen - Setzt Temperatur bei Zeitplan- oder Alarmänderungen +# Standardformat: service: climate.set_temperature | target: !input trv | data: safe_temperature actions: - choose: # Heizperiode Switch Aktionen - conditions: - condition: trigger id: - - HEATING_PERIOD_ON - - HEATING_PERIOD_OFF + - HEATING_PERIOD_STARTED + - HEATING_PERIOD_ENDED sequence: - choose: - conditions: - condition: trigger id: - - HEATING_PERIOD_ON + - HEATING_PERIOD_STARTED sequence: - service: climate.set_hvac_mode target: @@ -330,7 +405,7 @@ actions: - if: - condition: template value_template: > - {{ is_valid_temperature and is_temperature_change_needed }} + {{ is_valid_temperature and is_temperature_change_needed and is_state(window_detection_entity, 'off') }} then: - service: climate.set_temperature target: @@ -341,7 +416,7 @@ actions: - conditions: - condition: trigger id: - - HEATING_PERIOD_OFF + - HEATING_PERIOD_ENDED sequence: - service: switch.turn_off target: @@ -364,7 +439,7 @@ actions: - conditions: - condition: trigger id: - - FENSTER_OPEN + - WINDOW_OPENED - condition: template value_template: > {{ is_state(window_detection_entity, 'off') }} @@ -376,7 +451,7 @@ actions: - conditions: - condition: trigger id: - - FENSTER_CLOSED + - WINDOW_CLOSED - condition: template value_template: > {{ is_state(window_detection_entity, 'on') }} @@ -384,6 +459,8 @@ actions: - service: switch.turn_off target: entity_id: "{{ window_detection_entity }}" + - delay: + minutes: 1 - if: - condition: template value_template: > @@ -401,7 +478,7 @@ actions: - conditions: - condition: trigger id: - - TEMP_CHANGED + - SENSOR_TEMPERATURE_CHANGED - condition: template value_template: > {{ temperature_sensor is defined and states(temperature_sensor) | is_number }} @@ -417,13 +494,18 @@ actions: - conditions: - condition: trigger id: - - SYNC_TEMPERATURE + - PERIODIC_CHECK - condition: template value_template: > {{ temperature_sensor is defined and states(temperature_sensor) | is_number }} - condition: template value_template: > - {{ (now() - states[remote_temperature_entity].last_changed).total_seconds() > 1499 }} + {% if remote_temperature_last_change != none %} + {% set last_change = as_datetime(remote_temperature_last_change) %} + {{ last_change != none and (now() - last_change).total_seconds() > sensor_sync_max_age }} + {% else %} + false + {% endif %} sequence: - service: number.set_value data: @@ -432,18 +514,38 @@ actions: entity_id: "{{ remote_temperature_entity }}" alias: Synchronisiere Temperatur am TRV (zeitbasiert, wenn länger als 25min unverändert) alias: Temperatursynchronisation + # override reset + - choose: + - conditions: + - condition: trigger + id: + - PERIODIC_CHECK + - condition: template + value_template: "{{ override_reset_duration_seconds > 0 }}" + - condition: template + value_template: "{{ is_manual_override }}" + - condition: template + value_template: "{{ override_duration_exceeded }}" + sequence: + - service: climate.set_temperature + target: + entity_id: !input trv + data: + temperature: "{{ safe_temperature | float }}" + alias: Setze Solltemperatur zurück nach manuellem Override + alias: Override Reset # setze Solltemperatur bei Schedule und Alarm-Status-Änderungen - choose: - conditions: - condition: trigger id: - ALARM_ARMED_AWAY - - ALARM_DISARMED_AWAY - - SCHEDULE_TEMP_CHANGED - - SCHEDULER_CHANGED + - ALARM_DISARMED + - SCHEDULE_TEMPERATURE_CHANGED + - SCHEDULE_SELECTOR_CHANGED - condition: template value_template: > - {% if trigger.id == 'SCHEDULE_TEMP_CHANGED' %} + {% if trigger.id == 'SCHEDULE_TEMPERATURE_CHANGED' %} {% set selected_friendly_name = states(active_scheduler_selector) %} {{ state_attr(trigger.entity_id, 'friendly_name') == selected_friendly_name }} {% else %} @@ -453,7 +555,7 @@ actions: - if: - condition: template value_template: > - {{ is_valid_temperature and is_temperature_change_needed }} + {{ is_valid_temperature and is_temperature_change_needed and is_state(window_detection_entity, 'off') }} then: - service: climate.set_temperature target: