mode: queued max: 10 blueprint: name: Bosch BTH-RA Radiator Control homeassistant: 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: trv: name: Thermostat description: Thermostat muss ein Bosch BTH-RA sein selector: entity: multiple: false filter: - domain: climate temperature_sensor: name: Temperatursensor selector: entity: multiple: false 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. selector: entity: multiple: false filter: - domain: input_boolean min_temperature: name: Minimale Temperatur description: > Minimale erlaubte Solltemperatur als Sicherheitsgrenze (Default = 15°C). Alle berechneten Temperaturen werden auf diesen Minimalwert begrenzt (Clamping). default: 15 selector: number: mode: box min: 10.0 max: 20.0 unit_of_measurement: "°C" step: 0.5 max_temperature: name: Maximale Temperatur description: > Maximale erlaubte Solltemperatur als Sicherheitsgrenze (Default = 23°C). Alle berechneten Temperaturen werden auf diesen Maximalwert begrenzt (Clamping). default: 23 selector: number: mode: box min: 20.0 max: 25.0 unit_of_measurement: "°C" step: 0.5 window_section: name: Fenster-/Türsensor Konfiguration description: Konfiguration für den Fenster-/Türsensor. collapsed: true input: window_sensor: name: Fenster-/Türsensor (oder Gruppe) selector: entity: multiple: false filter: - domain: binary_sensor window_delay_open: name: Fenster-/Türsensor Verzögerung description: Zeit die das Fenster offen bleiben muss, um die den Radiator in den "Fenster offen"-Modus zu versetzen (Default = 30s) default: 30 selector: number: mode: box min: 0.0 max: 3600.0 unit_of_measurement: seconds step: 1.0 window_delay_close: name: Fenster-/Türsensor Verzögerung description: Zeit die das Fenster geschlossen bleiben muss, um die den Radiator in den "Fenster geschlossen"-Modus zu versetzen (Default = 5s) default: 5 selector: number: mode: box min: 0.0 max: 3600.0 unit_of_measurement: seconds step: 1.0 schedule_section: name: Heizplan Konfiguration description: Konfiguration der Heizpläne die für den Radiator berücksichtigt werden sollen collapsed: true input: radiator_schedules: name: Heizpläne description: Alle Heizpläne die für den Radiator berücksichtigt werden sollen. Sind mehrere Schedules aktiv, wird der erste mit einer gültigen Temperatur verwendet. default: [] selector: entity: multiple: true filter: - domain: schedule active_scheduler_selector: name: Aktiver Scheduler Selector description: > Input Select der den aktuell aktiven Scheduler enthält (per Friendly Name). Wird verwendet um zu bestimmen welcher Schedule für die Temperatur verwendet werden soll. **WICHTIG**: Die Options müssen die Friendly Names der Scheduler aus der Scheduler-Liste enthalten. **HINWEIS**: Wenn der angezeigte Scheduler inaktiv ist (off), wird automatisch auf die Abwesenheitstemperatur zurückgegriffen. Beispiel: Wird typischerweise vom "Heizplan Selector" Blueprint gesteuert. selector: entity: filter: - domain: input_select away_section: name: Konfiguration für Abwesenheitsmodus description: Konfiguration für das Absenken der Heizung im Abwesenheitsmodus eines Alarmsystems collapsed: true input: alarm_control_panel: name: Alarm Control Panel description: Optional - Alarm Control Panel um Heizung abzusenken, wenn Alarm im Abwesendmodus ist selector: entity: multiple: false filter: - domain: alarm_control_panel away_temperature: name: Abwesenheitstemperatur 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: mode: box min: 15.0 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 away_temperature: !input away_temperature min_temperature: !input min_temperature max_temperature: !input max_temperature alarm_control_panel: !input alarm_control_panel 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: > {{ device_entities(device_id(trv)) | select('search', 'remote_temperature') | list | first | default('') }} window_detection_entity: > {{ 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 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 %} # 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 != none %} {{ scheduled_temperature }} {% else %} {{ away_temperature }} {% endif %} # Begrenzt target_temperature auf den Bereich [min_temperature, max_temperature] (Clamping) safe_temperature: > {{ [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: > {{ (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 new_sensor_temp = states(temperature_sensor) | float(0) %} {% set target_temp = state_attr(trv, 'temperature') | float(0) %} {% 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 %} 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: - platform: state entity_id: - !input window_sensor from: "off" to: "on" for: !input window_delay_open id: WINDOW_OPENED - platform: state entity_id: - !input window_sensor from: "on" to: "off" for: !input window_delay_close id: WINDOW_CLOSED - platform: time_pattern # Periodische Überprüfung alle 5 Minuten (Temperatursynchronisation, Override-Check) minutes: "/5" id: PERIODIC_CHECK - platform: state entity_id: !input temperature_sensor id: SENSOR_TEMPERATURE_CHANGED - platform: state entity_id: !input alarm_control_panel to: "armed_away" id: ALARM_ARMED_AWAY - platform: state entity_id: !input alarm_control_panel from: "armed_away" to: "disarmed" id: ALARM_DISARMED - platform: state entity_id: !input heating_period_switch to: "on" id: HEATING_PERIOD_STARTED - platform: state entity_id: !input heating_period_switch to: "off" id: HEATING_PERIOD_ENDED - platform: state entity_id: !input radiator_schedules attribute: temp id: SCHEDULE_TEMPERATURE_CHANGED - platform: state entity_id: !input active_scheduler_selector 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_STARTED - HEATING_PERIOD_ENDED sequence: - choose: - conditions: - condition: trigger id: - HEATING_PERIOD_STARTED sequence: - service: climate.set_hvac_mode target: entity_id: !input trv data: hvac_mode: "heat" - if: - condition: state entity_id: !input window_sensor state: "on" then: - service: switch.turn_on target: entity_id: "{{ window_detection_entity }}" - if: - condition: template value_template: > {{ is_valid_temperature and is_temperature_change_needed and is_state(window_detection_entity, 'off') }} then: - service: climate.set_temperature target: entity_id: !input trv data: temperature: "{{ safe_temperature | float }}" alias: Heizperiode aktiviert - Setze Modus auf heat, synchronisiere Fensterstatus und Solltemperatur - conditions: - condition: trigger id: - HEATING_PERIOD_ENDED sequence: - service: switch.turn_off target: entity_id: "{{ window_detection_entity }}" - service: climate.set_hvac_mode target: entity_id: !input trv data: hvac_mode: "off" alias: Heizperiode deaktiviert - Setze Fenster geschlossen und Modus auf off alias: Heizperiode Switch Änderung alias: Heizperiode Switch Aktionen # Alle bestehenden Aktionen nur ausführen wenn Heizperiode aktiv ist - conditions: - condition: template value_template: "{{ is_heating_period }}" sequence: # setze Fenster auf offen/geschlossen - choose: - conditions: - condition: trigger id: - WINDOW_OPENED - condition: template value_template: > {{ is_state(window_detection_entity, 'off') }} sequence: - service: switch.turn_on target: entity_id: "{{ window_detection_entity }}" alias: Setze Fenster auf offen - conditions: - condition: trigger id: - WINDOW_CLOSED - condition: template value_template: > {{ is_state(window_detection_entity, 'on') }} sequence: - service: switch.turn_off target: entity_id: "{{ window_detection_entity }}" - delay: minutes: 1 - if: - condition: template value_template: > {{ is_valid_temperature and is_temperature_change_needed }} then: - service: climate.set_temperature target: entity_id: !input trv data: temperature: "{{ safe_temperature | float }}" alias: Setze Fenster auf geschlossen und setze Solltemperatur auf Wert aus Zeitplan (wenn vorhanden) alias: Fensterstatus Änderung # temperature sensor sync - choose: - conditions: - condition: trigger id: - SENSOR_TEMPERATURE_CHANGED - condition: template value_template: > {{ temperature_sensor is defined and states(temperature_sensor) | is_number }} - condition: template value_template: "{{ is_sensor_sync_needed }}" sequence: - service: number.set_value data: value: "{{ states(temperature_sensor) | float }}" target: entity_id: "{{ remote_temperature_entity }}" alias: Synchronisiere Temperatur am TRV (bei Änderung) - conditions: - condition: trigger id: - PERIODIC_CHECK - condition: template value_template: > {{ temperature_sensor is defined and states(temperature_sensor) | is_number }} - condition: template value_template: > {% 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: value: "{{ states(temperature_sensor) | float }}" target: 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 - SCHEDULE_TEMPERATURE_CHANGED - SCHEDULE_SELECTOR_CHANGED - condition: template value_template: > {% 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 %} true {% endif %} sequence: - if: - condition: template value_template: > {{ is_valid_temperature and is_temperature_change_needed and is_state(window_detection_entity, 'off') }} then: - service: climate.set_temperature target: entity_id: !input trv data: temperature: "{{ safe_temperature | float }}" # Notification bei Scheduler-Mismatch - if: - condition: template value_template: "{{ scheduler_mismatch and trigger.id == 'SCHEDULER_CHANGED' }}" then: - service: persistent_notification.create data: title: "⚠️ Radiator Control - Scheduler nicht gefunden" message: > Der ausgewählte Scheduler '{{ states(active_scheduler_selector) }}' wurde nicht in der Scheduler-Liste gefunden. TRV '{{ state_attr(trv, 'friendly_name') }}' nutzt Fallback-Temperatur: {{ safe_temperature }}°C (Original: {{ target_temperature }}°C, Limits: {{ min_temperature }}-{{ max_temperature }}°C) Bitte Konfiguration überprüfen. notification_id: "radiator_control_scheduler_mismatch_{{ trv }}" alias: Setze Solltemperatur alias: Solltemperatur bei Änderungen alias: Aktionen während Heizperiode alias: Hauptsteuerung