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

179 lines
6.6 KiB
YAML

blueprint:
name: Area Valve Temperature Offset Calibration
author: Andreas Gammelgaard Damsbo
description: |
Automatically calibrate the temperature offset of ALL smart radiator valves (TRVs)
in a specified area using readings from an external room temperature sensor.
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.
Example use case:
- **area**: Living Room
- **external_sensor**: sensor.living_room_temperature
- **rounding_step**: 1.0
The blueprint will find all thermostats in "Living Room" and calibrate each one
to match the external sensor reading. Thermostats without offset entities are
automatically skipped.
Manual correction:
Use the **manual_correction** slider to bias the calculated offset in cases where
valves tend to stop heating too early or overheat even after calibration.
- Positive value (e.g., +0.3 °C): Valves assume room is warmer → reduce heating sooner
- Negative value (e.g., -0.3 °C): Valves assume room is cooler → prolong heating
domain: automation
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 using
the external temperature sensor.
selector:
area: {}
external_sensor:
name: External temperature sensor
description: >
Select the temperature sensor that measures the actual room temperature
for this area. This should be a reliable sensor that provides accurate
ambient temperature readings.
Example: sensor.living_room_temperature
selector:
entity:
domain: sensor
device_class: temperature
min_interval:
name: Minimum time between updates
description: >
The minimum amount of time (in seconds) that must pass before offsets
can be updated again. This prevents valves from being recalibrated too often.
A typical value is 300 seconds (5 minutes).
default: 300
selector:
number:
min: 30
max: 3600
step: 1
mode: box
unit_of_measurement: s
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**.
Use this if valves still behave undesirably after calibration:
- **Positive value** (e.g., +0.1…+1.0 °C): Makes valves think it's **warmer** → **reduces** heating sooner
- **Negative value** (e.g., -0.1…-1.0 °C): Makes valves think it's **cooler** → **prolongs** heating
Recommended start: 0.0 °C. Adjust in small steps (±0.1 °C) and observe behavior.
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.
By default, looks for entities containing "_local_temperature_offset".
Only change this if your devices use a different naming pattern.
default: "_local_temperature_offset"
selector:
text: {}
mode: queued
max: 10
max_exceeded: silent
trigger:
- platform: state
entity_id: !input external_sensor
- platform: state
entity_id: climate.*
attribute: current_temperature
action:
# Rate limiting check
- condition: template
value_template: >
{% set last = state_attr(this.entity_id, 'last_triggered') %}
{{ last is none or (as_timestamp(now()) - as_timestamp(last)) > min_interval }}
# 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) }}"
# Skip if climate entity is unavailable or has no valid temperature
- condition: template
value_template: >
{{ states(climate_entity) not in ['unknown', 'unavailable'] and valve_temp > 0 and external_temp > 0 }}
# Find offset entity for this device
- 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 '' }}
# Skip if no offset entity found
- condition: template
value_template: "{{ offset_entity != '' }}"
- condition: template
value_template: "{{ states(offset_entity) not in ['unknown', 'unavailable'] }}"
# Calculate and set new offset
- variables:
current_offset: "{{ states(offset_entity) | float(0) }}"
raw_offset: "{{ external_temp - (valve_temp - current_offset) + manual_correction }}"
new_offset: "{{ ((raw_offset / rounding_step) | round(0)) * rounding_step }}"
- service: number.set_value
target:
entity_id: "{{ offset_entity }}"
data:
value: "{{ new_offset }}"
# Small delay between updates to avoid overwhelming the system
- delay:
milliseconds: 100
variables:
target_area: !input target_area
external_sensor: !input external_sensor
min_interval: !input min_interval
rounding_step: !input rounding_step
manual_correction: !input manual_correction
offset_entity_suffix: !input offset_entity_suffix