manual override reset plus minor fixes #3

Merged
pierre merged 31 commits from dev into main 2026-01-15 14:23:00 +01:00

View File

@@ -6,6 +6,11 @@ blueprint:
min_version: "2024.6.0" min_version: "2024.6.0"
description: > description: >
Eine Automation zur automatischen Steuerung eines Bosch TRV Heizkörperthermostats. 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 domain: automation
author: Me author: Me
input: input:
@@ -25,6 +30,24 @@ blueprint:
filter: filter:
- domain: sensor - domain: sensor
device_class: temperature 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: heating_period_switch:
name: Heizperiode Switch name: Heizperiode Switch
description: Optional - Input Boolean der angibt ob Heizperiode aktiv ist. Wenn nicht gesetzt, ist die Heizperiode immer aktiv. 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 - domain: input_boolean
min_temperature: min_temperature:
name: Minimale Temperatur 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 default: 15
selector: selector:
number: number:
@@ -46,7 +71,9 @@ blueprint:
step: 0.5 step: 0.5
max_temperature: max_temperature:
name: Maximale Temperatur 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 default: 23
selector: selector:
number: number:
@@ -58,7 +85,7 @@ blueprint:
window_section: window_section:
name: Fenster-/Türsensor Konfiguration name: Fenster-/Türsensor Konfiguration
description: Konfiguration für den Fenster-/Türsensor. description: Konfiguration für den Fenster-/Türsensor.
collapsed: false collapsed: true
input: input:
window_sensor: window_sensor:
name: Fenster-/Türsensor (oder Gruppe) name: Fenster-/Türsensor (oder Gruppe)
@@ -92,7 +119,7 @@ blueprint:
schedule_section: schedule_section:
name: Heizplan Konfiguration name: Heizplan Konfiguration
description: Konfiguration der Heizpläne die für den Radiator berücksichtigt werden sollen description: Konfiguration der Heizpläne die für den Radiator berücksichtigt werden sollen
collapsed: false collapsed: true
input: input:
radiator_schedules: radiator_schedules:
name: Heizpläne name: Heizpläne
@@ -123,7 +150,7 @@ blueprint:
away_section: away_section:
name: Konfiguration für Abwesenheitsmodus name: Konfiguration für Abwesenheitsmodus
description: Konfiguration für das Absenken der Heizung im Abwesenheitsmodus eines Alarmsystems description: Konfiguration für das Absenken der Heizung im Abwesenheitsmodus eines Alarmsystems
collapsed: false collapsed: true
input: input:
alarm_control_panel: alarm_control_panel:
name: Alarm Control Panel name: Alarm Control Panel
@@ -135,7 +162,12 @@ blueprint:
- domain: alarm_control_panel - domain: alarm_control_panel
away_temperature: away_temperature:
name: Abwesenheitstemperatur 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 default: 18
selector: selector:
number: number:
@@ -144,10 +176,39 @@ blueprint:
max: 25.0 max: 25.0
unit_of_measurement: "°C" unit_of_measurement: "°C"
step: 0.5 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: variables:
# Input-Variablen
trv: !input trv trv: !input trv
temperature_sensor: !input temperature_sensor temperature_sensor: !input temperature_sensor
sensor_sync_threshold: !input sensor_sync_threshold
heating_period_switch: !input heating_period_switch heating_period_switch: !input heating_period_switch
radiator_schedules: !input radiator_schedules radiator_schedules: !input radiator_schedules
active_scheduler_selector: !input active_scheduler_selector active_scheduler_selector: !input active_scheduler_selector
@@ -155,102 +216,110 @@ variables:
min_temperature: !input min_temperature min_temperature: !input min_temperature
max_temperature: !input max_temperature max_temperature: !input max_temperature
alarm_control_panel: !input alarm_control_panel alarm_control_panel: !input alarm_control_panel
is_heating_period: > override_reset_duration: !input override_reset_duration
{% if heating_period_switch is none or heating_period_switch == '' %}
true # Konstanten
{% else %} temperature_change_tolerance: 0.4 # °C - Minimale Differenz für Temperaturänderung
{{ is_state(heating_period_switch, 'on') }} sensor_sync_max_age: 1499 # Sekunden (25 Minuten - 1 Sekunde) - Max Alter für Sensor-Sync
{% endif %}
# Entity-Discovery
remote_temperature_entity: > remote_temperature_entity: >
{% set entities = device_entities(device_id(trv)) %} {{ device_entities(device_id(trv)) | select('search', 'remote_temperature') | list | first | default('') }}
{% 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 }}
window_detection_entity: > window_detection_entity: >
{% set entities = device_entities(device_id(trv)) %} {{ device_entities(device_id(trv)) | select('search', 'window_detection') | list | first | default('') }}
{% set window_detection_entity_id = namespace(id='') %} setpoint_change_source_entity: >
{% for entity in entities %} {{ device_entities(device_id(trv)) | select('search', 'setpoint_change_source') | list | first | default('') }}
{% if 'window_detection' in entity %}
{% set window_detection_entity_id.id = entity %} # Entity-Validierung
{% endif %} remote_temperature_entity_valid: >
{% endfor %} {{ remote_temperature_entity and states(remote_temperature_entity) not in ['unknown', 'unavailable', none] }}
{{ window_detection_entity_id.id }} 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: > scheduled_temperature: >
{% set ns = namespace(current_temperature = none) %} {% set selected = states(active_scheduler_selector) %}
{% set selected_friendly_name = states(active_scheduler_selector) %} {% if selected not in [none, 'unknown', ''] %}
{% if selected_friendly_name is not none and selected_friendly_name != 'unknown' %} {% for schedule in radiator_schedules if state_attr(schedule, 'friendly_name') == selected and is_state(schedule, 'on') %}
{% for schedule in radiator_schedules %}
{% if state_attr(schedule, 'friendly_name') == selected_friendly_name and states(schedule) == 'on' %}
{% set temp = state_attr(schedule, 'temp') %} {% set temp = state_attr(schedule, 'temp') %}
{% if temp is not none and temp | is_number %} {% if temp is not none and temp | is_number %}
{% set ns.current_temperature = temp %} {{ temp }}
{% break %} {% break %}
{% endif %} {% endif %}
{% endif %}
{% endfor %} {% endfor %}
{% endif %} {% 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: > target_temperature: >
{% if alarm_control_panel and is_state(alarm_control_panel, 'armed_away') %} {% if alarm_control_panel and is_state(alarm_control_panel, 'armed_away') %}
{{ away_temperature }} {{ away_temperature }}
{% elif scheduled_temperature is not none %} {% elif scheduled_temperature != none %}
{{ scheduled_temperature }} {{ scheduled_temperature }}
{% else %} {% else %}
{{ away_temperature }} {{ away_temperature }}
{% endif %} {% endif %}
# Begrenzt target_temperature auf den Bereich [min_temperature, max_temperature] (Clamping)
safe_temperature: > safe_temperature: >
{% set temp = target_temperature | float(18) %} {{ [min_temperature, [max_temperature, target_temperature | float(18)] | min] | max }}
{% if temp < min_temperature %}
{{ min_temperature }} # Status-Prüfungen
{% elif temp > max_temperature %}
{{ max_temperature }}
{% else %}
{{ temp }}
{% endif %}
is_valid_temperature: > is_valid_temperature: >
{{ safe_temperature is not none and safe_temperature | is_number }} {{ safe_temperature is not none and safe_temperature | is_number }}
is_temperature_change_needed: > is_temperature_change_needed: >
{% set current = state_attr(trv, 'temperature') | float(0) %} {{ (safe_temperature | float(0) - state_attr(trv, 'temperature') | float(0)) | abs >= temperature_change_tolerance }}
{% set new = safe_temperature | float(0) %} # Synchronisation nötig wenn: 1) Differenz > Schwelle, 2) Sensor kreuzt Soll-Temp von unten, 3) Sensor kreuzt Soll-Temp von oben
{% set diff = new - current %}
{% if new > current %}
true
{% elif diff | abs >= 0.4 %}
true
{% else %}
false
{% endif %}
is_sensor_sync_needed: > is_sensor_sync_needed: >
{% set current_remote_temp = states(remote_temperature_entity) | float(0) %}
{% set new_sensor_temp = states(temperature_sensor) | float(0) %} {% set new_sensor_temp = states(temperature_sensor) | float(0) %}
{% set target_temp = state_attr(trv, 'temperature') | float(0) %} {% set target_temp = state_attr(trv, 'temperature') | float(0) %}
{% set diff = (new_sensor_temp - current_remote_temp) | abs %} {% set diff = (new_sensor_temp - current_remote_temperature) | abs %}
{% if diff >= 0.5 %} {{ diff >= sensor_sync_threshold or
true (new_sensor_temp < target_temp and current_remote_temperature >= target_temp) or
{% elif new_sensor_temp < target_temp and current_remote_temp >= target_temp %} (new_sensor_temp > target_temp and current_remote_temperature <= target_temp) }}
true
{% elif new_sensor_temp > target_temp and current_remote_temp <= target_temp %}
true
{% else %}
false
{% endif %}
scheduler_mismatch: > scheduler_mismatch: >
{% set selected_friendly_name = states(active_scheduler_selector) %} {% set selected = states(active_scheduler_selector) %}
{% if selected_friendly_name not in ['unknown', '', none] %} {% if selected not in ['unknown', '', none] %}
{% set found = namespace(value=false) %} {% set schedule_found = namespace(value=false) %}
{% for schedule in radiator_schedules %} {% for schedule in radiator_schedules %}
{% if state_attr(schedule, 'friendly_name') == selected_friendly_name %} {% if state_attr(schedule, 'friendly_name') == selected %}
{% set found.value = true %} {% set schedule_found.value = true %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{{ not found.value }} {{ not schedule_found.value }}
{% else %} {% else %}
false false
{% endif %} {% 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: triggers:
- platform: state - platform: state
@@ -259,21 +328,21 @@ triggers:
from: "off" from: "off"
to: "on" to: "on"
for: !input window_delay_open for: !input window_delay_open
id: FENSTER_OPEN id: WINDOW_OPENED
- platform: state - platform: state
entity_id: entity_id:
- !input window_sensor - !input window_sensor
from: "on" from: "on"
to: "off" to: "off"
for: !input window_delay_close for: !input window_delay_close
id: FENSTER_CLOSED id: WINDOW_CLOSED
- platform: time_pattern - platform: time_pattern
# Synce Temperatur alle 5 Minuten (Teiler von 60) # Periodische Überprüfung alle 5 Minuten (Temperatursynchronisation, Override-Check)
minutes: "/5" minutes: "/5"
id: SYNC_TEMPERATURE id: PERIODIC_CHECK
- platform: state - platform: state
entity_id: !input temperature_sensor entity_id: !input temperature_sensor
id: TEMP_CHANGED id: SENSOR_TEMPERATURE_CHANGED
- platform: state - platform: state
entity_id: !input alarm_control_panel entity_id: !input alarm_control_panel
to: "armed_away" to: "armed_away"
@@ -282,37 +351,43 @@ triggers:
entity_id: !input alarm_control_panel entity_id: !input alarm_control_panel
from: "armed_away" from: "armed_away"
to: "disarmed" to: "disarmed"
id: ALARM_DISARMED_AWAY id: ALARM_DISARMED
- platform: state - platform: state
entity_id: !input heating_period_switch entity_id: !input heating_period_switch
to: "on" to: "on"
id: HEATING_PERIOD_ON id: HEATING_PERIOD_STARTED
- platform: state - platform: state
entity_id: !input heating_period_switch entity_id: !input heating_period_switch
to: "off" to: "off"
id: HEATING_PERIOD_OFF id: HEATING_PERIOD_ENDED
- platform: state - platform: state
entity_id: !input radiator_schedules entity_id: !input radiator_schedules
attribute: temp attribute: temp
id: SCHEDULE_TEMP_CHANGED id: SCHEDULE_TEMPERATURE_CHANGED
- platform: state - platform: state
entity_id: !input active_scheduler_selector 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: actions:
- choose: - choose:
# Heizperiode Switch Aktionen # Heizperiode Switch Aktionen
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- HEATING_PERIOD_ON - HEATING_PERIOD_STARTED
- HEATING_PERIOD_OFF - HEATING_PERIOD_ENDED
sequence: sequence:
- choose: - choose:
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- HEATING_PERIOD_ON - HEATING_PERIOD_STARTED
sequence: sequence:
- service: climate.set_hvac_mode - service: climate.set_hvac_mode
target: target:
@@ -330,7 +405,7 @@ actions:
- if: - if:
- condition: template - condition: template
value_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: then:
- service: climate.set_temperature - service: climate.set_temperature
target: target:
@@ -341,7 +416,7 @@ actions:
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- HEATING_PERIOD_OFF - HEATING_PERIOD_ENDED
sequence: sequence:
- service: switch.turn_off - service: switch.turn_off
target: target:
@@ -364,7 +439,7 @@ actions:
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- FENSTER_OPEN - WINDOW_OPENED
- condition: template - condition: template
value_template: > value_template: >
{{ is_state(window_detection_entity, 'off') }} {{ is_state(window_detection_entity, 'off') }}
@@ -376,7 +451,7 @@ actions:
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- FENSTER_CLOSED - WINDOW_CLOSED
- condition: template - condition: template
value_template: > value_template: >
{{ is_state(window_detection_entity, 'on') }} {{ is_state(window_detection_entity, 'on') }}
@@ -384,6 +459,8 @@ actions:
- service: switch.turn_off - service: switch.turn_off
target: target:
entity_id: "{{ window_detection_entity }}" entity_id: "{{ window_detection_entity }}"
- delay:
minutes: 1
- if: - if:
- condition: template - condition: template
value_template: > value_template: >
@@ -401,7 +478,7 @@ actions:
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- TEMP_CHANGED - SENSOR_TEMPERATURE_CHANGED
- condition: template - condition: template
value_template: > value_template: >
{{ temperature_sensor is defined and states(temperature_sensor) | is_number }} {{ temperature_sensor is defined and states(temperature_sensor) | is_number }}
@@ -417,13 +494,18 @@ actions:
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- SYNC_TEMPERATURE - PERIODIC_CHECK
- condition: template - condition: template
value_template: > value_template: >
{{ temperature_sensor is defined and states(temperature_sensor) | is_number }} {{ temperature_sensor is defined and states(temperature_sensor) | is_number }}
- condition: template - condition: template
value_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: sequence:
- service: number.set_value - service: number.set_value
data: data:
@@ -432,18 +514,38 @@ actions:
entity_id: "{{ remote_temperature_entity }}" entity_id: "{{ remote_temperature_entity }}"
alias: Synchronisiere Temperatur am TRV (zeitbasiert, wenn länger als 25min unverändert) alias: Synchronisiere Temperatur am TRV (zeitbasiert, wenn länger als 25min unverändert)
alias: Temperatursynchronisation 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 # setze Solltemperatur bei Schedule und Alarm-Status-Änderungen
- choose: - choose:
- conditions: - conditions:
- condition: trigger - condition: trigger
id: id:
- ALARM_ARMED_AWAY - ALARM_ARMED_AWAY
- ALARM_DISARMED_AWAY - ALARM_DISARMED
- SCHEDULE_TEMP_CHANGED - SCHEDULE_TEMPERATURE_CHANGED
- SCHEDULER_CHANGED - SCHEDULE_SELECTOR_CHANGED
- condition: template - condition: template
value_template: > value_template: >
{% if trigger.id == 'SCHEDULE_TEMP_CHANGED' %} {% if trigger.id == 'SCHEDULE_TEMPERATURE_CHANGED' %}
{% set selected_friendly_name = states(active_scheduler_selector) %} {% set selected_friendly_name = states(active_scheduler_selector) %}
{{ state_attr(trigger.entity_id, 'friendly_name') == selected_friendly_name }} {{ state_attr(trigger.entity_id, 'friendly_name') == selected_friendly_name }}
{% else %} {% else %}
@@ -453,7 +555,7 @@ actions:
- if: - if:
- condition: template - condition: template
value_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: then:
- service: climate.set_temperature - service: climate.set_temperature
target: target: