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. Example use case: - **area**: Living Room - **external_sensor**: sensor.living_room_temperature - **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 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" 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) }}" # 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 or is unavailable - condition: template 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) }}" # 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 }}" 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 run_interval: !input run_interval rounding_step: !input rounding_step manual_correction: !input manual_correction offset_entity_suffix: !input offset_entity_suffix