diff --git a/ha-thermostat-calibration.yaml b/ha-thermostat-calibration.yaml index 2887372..edb469f 100644 --- a/ha-thermostat-calibration.yaml +++ b/ha-thermostat-calibration.yaml @@ -1,10 +1,13 @@ blueprint: name: Area Valve Temperature Offset Calibration - author: Andreas Gammelgaard Damsbo + 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. @@ -12,18 +15,7 @@ blueprint: 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 + - **run_interval**: /10 (every 10 minutes) domain: automation source_url: https://gdamsbo.dk/forgejo/andreas/ha-thermostat-calibration/raw/branch/main/ha-thermostat-calibration.yaml @@ -33,8 +25,7 @@ blueprint: 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. + All climate entities in this area will be automatically calibrated. selector: area: {} @@ -42,28 +33,20 @@ blueprint: 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 + for this area. selector: entity: domain: sensor device_class: temperature - min_interval: - name: Minimum time between updates + run_interval: + name: Run Interval 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 + 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: - number: - min: 30 - max: 3600 - step: 1 - mode: box - unit_of_measurement: s + text: {} rounding_step: name: Rounding step for offset value @@ -83,10 +66,8 @@ blueprint: 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. + 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: @@ -100,8 +81,7 @@ blueprint: 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" default: "_local_temperature_offset" selector: text: {} @@ -111,19 +91,10 @@ max: 10 max_exceeded: silent trigger: - - platform: state - entity_id: !input external_sensor - - platform: state - entity_id: climate.* - attribute: current_temperature + - platform: time_pattern + minutes: !input run_interval 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: > @@ -147,19 +118,30 @@ action: {% 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 + # Skip if no offset entity found or is unavailable - condition: template - value_template: "{{ offset_entity != '' }}" - - - condition: template - value_template: "{{ states(offset_entity) not in ['unknown', 'unavailable'] }}" + value_template: "{{ offset_entity != '' and 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 }}" + + # 1. Calculate the required offset based on the difference + # Required Offset = External Temp - (Valve Internal Temp - Current Offset) + Manual Correction + raw_offset_required: "{{ external_temp - (valve_temp - current_offset) + manual_correction }}" + + # 2. Round the raw offset to the nearest rounding_step + # Safely scale, round, and scale back to minimize floating point errors + new_offset: > + {% set step = rounding_step | float(1.0) %} + {% set scale = (1 / step) | round(0) %} + {{ (raw_offset_required * scale) | round(0) / scale }} + # Only update if the calculated value is different from the current value + - condition: template + value_template: "{{ new_offset | round(3) != current_offset | round(3) }}" + + # Set new offset - service: number.set_value target: entity_id: "{{ offset_entity }}" @@ -173,7 +155,7 @@ action: variables: target_area: !input target_area external_sensor: !input external_sensor - min_interval: !input min_interval + run_interval: !input run_interval rounding_step: !input rounding_step manual_correction: !input manual_correction - offset_entity_suffix: !input offset_entity_suffix + offset_entity_suffix: !input offset_entity_suffix \ No newline at end of file