ha-thermostat-calibration/ha-thermostat-calibration.yaml

168 lines
6.3 KiB
YAML
Raw Permalink Normal View History

blueprint:
name: Area Valve Temperature Offset Calibration
author: Andreas Gammelgaard Damsbo (Revised by AI)
description: |
Automatically calibrate the temperature offset of ALL smart radiator valves (TRVs)
in a specified area using readings from an external room temperature sensor.
This version runs on a time schedule and only updates the offset if the calculated
value differs from the current value on the valve, reducing unnecessary communication.
The blueprint discovers all climate entities in the chosen area and updates their
corresponding temperature offset entities based on the difference between each
valve's internal sensor and the actual room temperature.
2025-12-06 11:46:36 +01:00
Example use case:
- **area**: Living Room
- **external_sensor**: sensor.living_room_temperature
- **run_interval**: /10 (every 10 minutes)
domain: automation
2025-12-06 11:46:36 +01:00
source_url: https://gdamsbo.dk/forgejo/andreas/ha-thermostat-calibration/raw/branch/main/ha-thermostat-calibration.yaml
input:
target_area:
name: Area
description: >
Select the area containing the thermostatic valves you want to calibrate.
All climate entities in this area will be automatically calibrated.
selector:
area: {}
external_sensor:
name: External temperature sensor
description: >
Select the temperature sensor that measures the actual room temperature
for this area.
selector:
entity:
domain: sensor
device_class: temperature
run_interval:
name: Run Interval
description: >
How often to check for offset updates. Format is a time pattern string
(e.g., '/10' for every 10 minutes, '0' for every hour on the hour).
default: "/10"
selector:
text: {}
rounding_step:
name: Rounding step for offset value
description: >
Define the step to which calculated offsets should be rounded.
Use 0.5 for valves that support half-degree steps, or 1.0 for full degrees.
default: 1.0
selector:
number:
min: 0.1
max: 2
step: 0.1
mode: box
unit_of_measurement: "°C"
manual_correction:
name: Manual correction (bias)
description: >
Additional manual bias applied to computed offsets **before rounding**.
Positive value makes valves think it's warmer (reduces heating sooner).
Negative value makes valves think it's cooler (prolongs heating).
default: 0.0
selector:
number:
min: -1.0
max: 1.0
step: 0.1
mode: slider
unit_of_measurement: "°C"
offset_entity_suffix:
name: Offset entity suffix pattern
description: >
The text pattern used to identify offset number entities.
Default: "_local_temperature_offset"
2025-12-06 11:46:36 +01:00
default: "_local_temperature_offset"
selector:
text: {}
mode: queued
max: 10
max_exceeded: silent
trigger:
- platform: time_pattern
minutes: !input run_interval
action:
# Process each climate entity in the area
- repeat:
for_each: >
{{ area_entities(target_area) | select('match', '^climate\.') | list }}
sequence:
- variables:
climate_entity: "{{ repeat.item }}"
climate_device: "{{ device_id(climate_entity) }}"
external_temp: "{{ states(external_sensor) | float(0) }}"
valve_temp: "{{ state_attr(climate_entity, 'current_temperature') | float(0) }}"
hvac_action: "{{ state_attr(climate_entity, 'hvac_action') }}" # Get valve state (heating/idle)
target_temp: "{{ state_attr(climate_entity, 'temperature') | float(0) }}" # Get setpoint
# 1. Standard Skip Conditions
- condition: template
value_template: >
{{ states(climate_entity) not in ['unknown', 'unavailable'] and valve_temp > 0 and external_temp > 0 }}
# 2. **CRITICAL STABILITY CONDITION**
# Only calibrate if the valve is not actively heating and the setpoint is close to external temp
- condition: template
value_template: >
{{ hvac_action != 'heating' and (target_temp - external_temp) < 0.5 }}
# Find offset entity for this device (unchanged)
- variables:
offset_entity: >
{% set entities = device_entities(climate_device) if climate_device else [] %}
{% set offset_entities = entities | select('match', '^number\.') | select('search', offset_entity_suffix) | list %}
{{ offset_entities[0] if offset_entities else '' }}
# 3. Skip if no offset entity found (unchanged)
- condition: template
value_template: "{{ offset_entity != '' and states(offset_entity) not in ['unknown', 'unavailable'] }}"
# 4. Calculate New Offset using Differential Method
- variables:
current_offset: "{{ states(offset_entity) | float(0) }}"
# The new required offset is the difference between the external (ground truth)
# and the uncompensated internal sensor reading.
raw_offset_required: "{{ external_temp - valve_temp + manual_correction }}"
# Round the raw offset to the nearest rounding_step (unchanged rounding logic)
new_offset: >
{% set step = rounding_step | float(1.0) %}
{% set scale = (1 / step) | round(0) %}
{{ (raw_offset_required * scale) | round(0) / scale }}
# 5. Only update if the calculated value is different from the current value (unchanged)
- condition: template
value_template: "{{ new_offset | round(3) != current_offset | round(3) }}"
# 6. Set new offset (unchanged)
- service: number.set_value
target:
entity_id: "{{ offset_entity }}"
data:
value: "{{ new_offset }}"
# Small delay between updates (unchanged)
- delay:
milliseconds: 100
variables:
target_area: !input target_area
external_sensor: !input external_sensor
run_interval: !input run_interval
rounding_step: !input rounding_step
manual_correction: !input manual_correction
offset_entity_suffix: !input offset_entity_suffix