diff --git a/Climate/bosch_bth-ra_control.yaml b/Climate/bosch_bth-ra_control.yaml index 2b29fd4..0fd1160 100644 --- a/Climate/bosch_bth-ra_control.yaml +++ b/Climate/bosch_bth-ra_control.yaml @@ -1,4 +1,5 @@ -mode: single +mode: queued +max: 10 blueprint: name: Bosch BTH-RA Radiator Control homeassistant: @@ -32,6 +33,28 @@ blueprint: multiple: false filter: - domain: input_boolean + min_temperature: + name: Minimale Temperatur + description: Minimale erlaubte Solltemperatur als Sicherheitsgrenze (Default = 15°C) + 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 = 28°C) + 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. @@ -73,13 +96,30 @@ blueprint: 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 einem gültigen Slot verwendet. + 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: switch + - 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 @@ -110,7 +150,10 @@ variables: temperature_sensor: !input temperature_sensor 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 is_heating_period: > {% if heating_period_switch is none or heating_period_switch == '' %} @@ -138,19 +181,18 @@ variables: {{ window_detection_entity_id.id }} scheduled_temperature: > {% set ns = namespace(current_temperature = none) %} - {% for schedule in radiator_schedules %} - {% if states(schedule) == 'on' and state_attr(schedule, 'current_slot') is not none %} - {% set current_slot = state_attr(schedule, 'current_slot') %} - {% set actions = state_attr(schedule, 'actions') %} - {% if actions is not none and actions | length > current_slot %} - {% set action = actions[current_slot] %} - {% if action.data.temperature is defined and action.data.temperature | is_number %} - {% set ns.current_temperature = action.data.temperature %} + {% 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 %} {% endif %} - {% endif %} - {% endfor %} + {% endfor %} + {% endif %} {{ ns.current_temperature }} target_temperature: > {% if alarm_control_panel and is_state(alarm_control_panel, 'armed_away') %} @@ -160,6 +202,55 @@ variables: {% else %} {{ away_temperature }} {% endif %} + safe_temperature: > + {% set temp = target_temperature | float(18) %} + {% if temp < min_temperature %} + {{ min_temperature }} + {% elif temp > max_temperature %} + {{ max_temperature }} + {% else %} + {{ temp }} + {% endif %} + 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.5 %} + true + {% else %} + false + {% endif %} + 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 + {% 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 %} + false + {% endif %} triggers: - platform: state @@ -200,6 +291,13 @@ triggers: entity_id: !input heating_period_switch to: "off" id: HEATING_PERIOD_OFF + - platform: state + entity_id: !input radiator_schedules + attribute: temp + id: SCHEDULE_TEMP_CHANGED + - platform: state + entity_id: !input active_scheduler_selector + id: SCHEDULER_CHANGED actions: - choose: @@ -232,13 +330,13 @@ actions: - if: - condition: template value_template: > - {{ target_temperature is not none and target_temperature | is_number }} + {{ is_valid_temperature and is_temperature_change_needed }} then: - service: climate.set_temperature target: entity_id: !input trv data: - temperature: "{{ target_temperature | float }}" + temperature: "{{ safe_temperature | float }}" alias: Heizperiode aktiviert - Setze Modus auf heat, synchronisiere Fensterstatus und Solltemperatur - conditions: - condition: trigger @@ -289,13 +387,13 @@ actions: - if: - condition: template value_template: > - {{ target_temperature is not none and target_temperature | is_number }} + {{ is_valid_temperature and is_temperature_change_needed }} then: - service: climate.set_temperature target: entity_id: !input trv data: - temperature: "{{ target_temperature | float }}" + temperature: "{{ safe_temperature | float }}" alias: Setze Fenster auf geschlossen und setze Solltemperatur auf Wert aus Zeitplan (wenn vorhanden) alias: Fensterstatus Änderung # temperature sensor sync @@ -307,6 +405,8 @@ actions: - 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: @@ -323,34 +423,61 @@ actions: {{ temperature_sensor is defined and states(temperature_sensor) | is_number }} - condition: template value_template: > - {{ (now() - states[temperature_sensor].last_changed).total_seconds() > 1199 }} + {{ (now() - states[remote_temperature_entity].last_changed).total_seconds() > 1499 }} 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 20min unverändert) + alias: Synchronisiere Temperatur am TRV (zeitbasiert, wenn länger als 25min unverändert) alias: Temperatursynchronisation - # setze Solltemperatur basierend auf Alarm-Status + # setze Solltemperatur bei Schedule und Alarm-Status-Änderungen - choose: - conditions: - condition: trigger id: - ALARM_ARMED_AWAY - ALARM_DISARMED_AWAY + - SCHEDULE_TEMP_CHANGED + - SCHEDULER_CHANGED + - condition: template + value_template: > + {% if trigger.id == 'SCHEDULE_TEMP_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: > - {{ target_temperature is not none and target_temperature | is_number }} + {{ is_valid_temperature and is_temperature_change_needed }} then: - service: climate.set_temperature target: entity_id: !input trv data: - temperature: "{{ target_temperature | float }}" - alias: Setze Solltemperatur basierend auf Alarm-Status - alias: Solltemperatur setzen + 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 \ No newline at end of file diff --git a/Climate/schedule_selector.yaml b/Climate/schedule_selector.yaml new file mode 100644 index 0000000..92f87b4 --- /dev/null +++ b/Climate/schedule_selector.yaml @@ -0,0 +1,268 @@ +blueprint: + name: Heizplan Selector basierend auf Kalenderereignissen + description: > + Wählt automatisch einen Heizplan basierend auf Kalenderereignissen aus. + + Der Blueprint überwacht einen Kalender und wechselt zwischen verschiedenen + Schedule-Helfern basierend auf Keywords im Event-Titel oder der Beschreibung. + + **WICHTIG**: Die Anzahl der Scheduler und Keywordlisten muss übereinstimmen! + Jede Zeile in den Keywordlisten entspricht einem Scheduler an der gleichen Position. + + Bei Event-Ende wird zum Standard-Scheduler zurückgekehrt, außer ein anderes + Event ist bereits aktiv. + + Required = * + domain: automation + homeassistant: + min_version: "2024.6.0" + author: Me + input: + calendar_section: + name: Kalender-Einstellungen + description: Kalender und zeitlicher Offset für Events + collapsed: false + input: + calendar_entity: + name: Kalender * + description: Der Kalender, der für Event-Erkennung überwacht wird + selector: + entity: + filter: + - domain: calendar + + event_offset: + name: Zeitlicher Offset + description: > + Optional: Zeitlicher Versatz für Event-Start und -Ende. + Beispiel: "-00:15:00" startet 15 Minuten vor dem Event. + default: "00:00:00" + selector: + duration: + enable_day: false + + scheduler_section: + name: Scheduler-Konfiguration + description: Schedule-Helfer und zugeordnete Keywords + collapsed: false + input: + scheduler_list: + name: Scheduler-Liste * + description: > + Liste der Schedule-Helfer (schedule entities). + Die Reihenfolge muss mit den Keywordlisten übereinstimmen! + selector: + entity: + multiple: true + filter: + - domain: schedule + + keyword_lists: + name: Keywordlisten * + description: > + Eine Keywordliste pro Zeile (komma-separiert). + Beispiel: + urlaub,vacation,holiday + homeoffice,wfh,remote + normal,standard + + **Position in der Liste = Position im Scheduler!** + Zeile 1 → Scheduler 1, Zeile 2 → Scheduler 2, etc. + selector: + text: + multiline: true + type: text + + default_scheduler: + name: Standard-Scheduler * + description: > + Dieser Scheduler wird aktiviert, wenn kein Keyword matched + oder ein Event endet (und kein anderes aktiv ist). + selector: + entity: + filter: + - domain: schedule + + helper_section: + name: Helper-Entity + description: Entity zum Speichern des aktiven Schedulers + collapsed: false + input: + helper_entity: + name: Input Select Helper * + description: > + Die input_select Helper-Entity, in die der aktive Scheduler geschrieben wird. + + **WICHTIG**: Die Options der input_select müssen vorbereitet werden! + Sie müssen die Friendly Names folgender Entities enthalten: + - Alle Scheduler aus der Scheduler-Liste + - Den Standard-Scheduler + + Beispiel Options: + ["Urlaub Heizplan", "Home Office Heizplan", "Normal Heizplan", "Standard Heizplan"] + + Die Blueprint schreibt den Friendly Name (nicht die Entity-ID) in die Helper-Entity. + selector: + entity: + filter: + - domain: input_select + +mode: single +max_exceeded: silent + +triggers: + - platform: calendar + event: start + entity_id: !input calendar_entity + offset: !input event_offset + id: CALENDAR_START + + - platform: calendar + event: end + entity_id: !input calendar_entity + offset: !input event_offset + id: CALENDAR_END + +variables: + schedulers: !input scheduler_list + keywords_raw: !input keyword_lists + default_scheduler: !input default_scheduler + helper: !input helper_entity + calendar: !input calendar_entity + + # Keywords in Liste umwandeln (eine pro Zeile) + keyword_lists: > + {{ keywords_raw.split('\n') | select() | list }} + + # Bei Event-Start: Keywords matchen + matched_scheduler: > + {% set ns = namespace(scheduler = none) %} + {% if trigger.id == 'CALENDAR_START' %} + {% set event_text = (trigger.calendar_event.summary | lower) ~ ' ' ~ (trigger.calendar_event.description | default('') | lower) %} + {% for i in range(keyword_lists | length) %} + {% set keywords = keyword_lists[i].split(',') | map('trim') | map('lower') | list %} + {% set matched = namespace(found = false) %} + {% for keyword in keywords %} + {% if keyword in event_text %} + {% set matched.found = true %} + {% endif %} + {% endfor %} + {% if matched.found and i < (schedulers | length) %} + {% set ns.scheduler = schedulers[i] %} + {% break %} + {% endif %} + {% endfor %} + {% endif %} + {{ ns.scheduler }} + + # Bei Event-Ende: Prüfen ob ein anderes Event aktiv ist + other_event_active: > + {% if trigger.id == 'CALENDAR_END' %} + {% set start = state_attr(calendar, 'start_time') %} + {% set end = state_attr(calendar, 'end_time') %} + {% if start and end %} + {% set now_ts = now().timestamp() %} + {% set start_ts = as_timestamp(start) %} + {% set end_ts = as_timestamp(end) %} + {{ start_ts <= now_ts and now_ts <= end_ts }} + {% else %} + false + {% endif %} + {% else %} + false + {% endif %} + +conditions: [] + +actions: + - choose: + # Event-Start: Scheduler aktivieren wenn Keyword matched + - conditions: + - condition: trigger + id: CALENDAR_START + - condition: template + value_template: "{{ matched_scheduler is not none and matched_scheduler != '' }}" + sequence: + - if: + - condition: template + value_template: > + {{ state_attr(matched_scheduler, 'friendly_name') in state_attr(helper, 'options') }} + then: + # Matched Scheduler ist in Options vorhanden + - service: input_select.select_option + target: + entity_id: !input helper_entity + data: + option: "{{ state_attr(matched_scheduler, 'friendly_name') }}" + else: + # Matched Scheduler nicht vorhanden - Fallback auf Default + - if: + - condition: template + value_template: > + {{ state_attr(default_scheduler, 'friendly_name') in state_attr(helper, 'options') }} + then: + - service: input_select.select_option + target: + entity_id: !input helper_entity + data: + option: "{{ state_attr(default_scheduler, 'friendly_name') }}" + - service: persistent_notification.create + data: + title: "⚠️ Heizplan Selector - Konfigurationsfehler" + message: > + Der gematchte Scheduler '{{ state_attr(matched_scheduler, 'friendly_name') }}' ({{ matched_scheduler }}) ist nicht in den Options der input_select '{{ helper }}' vorhanden. + + Fallback auf Standard-Scheduler '{{ state_attr(default_scheduler, 'friendly_name') }}' ({{ default_scheduler }}). + + Event: {{ trigger.calendar_event.summary }} + notification_id: "schedule_selector_config_error" + else: + # Auch Default-Scheduler nicht vorhanden + - service: persistent_notification.create + data: + title: "❌ Heizplan Selector - Kritischer Konfigurationsfehler" + message: > + Weder der gematchte Scheduler '{{ state_attr(matched_scheduler, 'friendly_name') }}' ({{ matched_scheduler }}) noch der Standard-Scheduler '{{ state_attr(default_scheduler, 'friendly_name') }}' ({{ default_scheduler }}) sind in den Options der input_select '{{ helper }}' vorhanden. + + Kein Scheduler wurde aktiviert! + + Bitte input_select Options korrigieren. + + Event: {{ trigger.calendar_event.summary }} + notification_id: "schedule_selector_critical_error" + alias: Event-Start - Scheduler aktivieren + + # Event-Ende: Zu Standard zurückkehren (nur wenn kein anderes Event aktiv) + - conditions: + - condition: trigger + id: CALENDAR_END + - condition: template + value_template: "{{ not other_event_active }}" + sequence: + - if: + - condition: template + value_template: > + {{ state_attr(default_scheduler, 'friendly_name') in state_attr(helper, 'options') }} + then: + - service: input_select.select_option + target: + entity_id: !input helper_entity + data: + option: "{{ state_attr(default_scheduler, 'friendly_name') }}" + else: + - service: persistent_notification.create + data: + title: "❌ Heizplan Selector - Konfigurationsfehler" + message: > + Der Standard-Scheduler '{{ state_attr(default_scheduler, 'friendly_name') }}' ({{ default_scheduler }}) ist nicht in den Options der input_select '{{ helper }}' vorhanden. + + Kein Scheduler wurde aktiviert! + + Bitte input_select Options korrigieren. + + Event: {{ trigger.calendar_event.summary }} + notification_id: "schedule_selector_default_error" + alias: Event-Ende - Standard-Scheduler aktivieren + + default: [] +