blueprint: name: Adaptive Comfort Control Pro v4.23.5 description: 'Adaptive comfort control + HVAC pause with unit awareness (°C/°F), seasonal bias, built-in psychrometrics (dew point, absolute humidity, enthalpy), optional RMOT, and CO₂-driven ventilation preference. Includes per-field help text and regional presets (seasonal bias + ventilation/psychrometrics/CO₂) by U.S. state or region. ' domain: automation author: Jeremy Carter source_url: https://raw.githubusercontent.com/schoolboyqueue/home-assistant-blueprints/main/blueprints/adaptive-comfort-control/adaptive_comfort_control_pro_blueprint.yaml input: core: name: Core icon: mdi:home-thermometer collapsed: false input: climate_entity: name: Climate Device description: Your thermostat or climate entity to control (e.g., Ecobee, Generic Thermostat, SmartIR). selector: entity: domain: - climate multiple: false reorder: false indoor_temp_sensor: name: Indoor Temperature description: Primary indoor air temperature for the room (sensor/input_number/weather). selector: entity: domain: - sensor - input_number - weather multiple: false reorder: false outdoor_temp_sensor: name: Outdoor Temperature description: Outdoor temperature used by the ASHRAE-55 adaptive model (sensor/input_number/weather). selector: entity: domain: - sensor - input_number - weather multiple: false reorder: false optional_sensors: name: Optional Sensors icon: mdi:plus-circle collapsed: true input: mean_radiant_temp_sensor: name: Mean Radiant Temperature description: If provided, operative temperature = mean(indoor air, mean radiant). Useful near large windows or radiant sources. default: sensor.none selector: entity: domain: - sensor - input_number multiple: false reorder: false indoor_humidity_sensor: name: Indoor Humidity description: Indoor RH (%) for psychrometrics (dew point, AH, enthalpy). If blank, assumes 50%. default: sensor.none selector: entity: domain: - sensor - input_number multiple: false reorder: false outdoor_humidity_sensor: name: Outdoor Humidity description: Outdoor RH (%) for psychrometrics. If blank, assumes 50%. default: sensor.none selector: entity: domain: - sensor - input_number - weather multiple: false reorder: false occupancy_sensor: name: Occupancy description: Presence/motion sensor to bias behavior when home is occupied (optional). default: binary_sensor.none selector: entity: domain: - binary_sensor device_class: - motion - occupancy - presence multiple: false reorder: false sun_entity: name: Sun Entity description: Sun entity for day/night detection in learning. Uses sun.sun by default, which tracks actual sunrise/sunset for your location. This ensures learning adapts as daylight hours change with seasons. default: sun.sun selector: entity: domain: - sun multiple: false reorder: false comfort: name: Comfort & Units icon: mdi:tune-variant collapsed: false input: units_override: name: Units description: Auto-detect by default. You can force °C or °F if your sensors/climate are mixed. ⚠️ When set to 'c', use °C fields below. When 'f', use °F fields. When 'auto', use whichever matches your system. default: auto selector: select: options: - label: Auto-detect from sensors value: auto - label: Force Celsius (°C) value: c - label: Force Fahrenheit (°F) value: f custom_value: false multiple: false sort: false comfort_category: name: ASHRAE 55 Category (tolerance) description: 'Controls comfort band width: I = ±2°C (office), II = ±3°C (typical home), III = ±4°C (max savings).' default: II selector: select: options: - label: Category I (±2°C / ±3.6°F) — ~90% satisfaction (office) value: I - label: Category II (±3°C / ±5.4°F) — ~80% satisfaction (typical home) value: II - label: Category III (±4°C / ±7.2°F) — ~65% satisfaction (max savings) value: III custom_value: false multiple: false sort: false min_comfort_temp_c: name: ❄️ Minimum Comfort (°C) — Metric Only description: Hard lower bound for the comfort band. ℹ️ Only used when Units='auto' with metric sensors, or Units='c'. Ignored if using °F. default: 18.0 selector: number: min: 10.0 max: 25.0 step: 0.5 unit_of_measurement: °C mode: slider max_comfort_temp_c: name: "\U0001F525 Maximum Comfort (°C) — Metric Only" description: Hard upper bound for the comfort band. ℹ️ Only used when Units='auto' with metric sensors, or Units='c'. Ignored if using °F. default: 28.0 selector: number: min: 20.0 max: 35.0 step: 0.5 unit_of_measurement: °C mode: slider min_comfort_temp_f: name: ❄️ Minimum Comfort (°F) — Imperial Only description: Hard lower bound for the comfort band. ℹ️ Only used when Units='auto' with imperial sensors, or Units='f'. Ignored if using °C. default: 64.0 selector: number: min: 50.0 max: 75.0 step: 0.5 unit_of_measurement: °F mode: slider max_comfort_temp_f: name: "\U0001F525 Maximum Comfort (°F) — Imperial Only" description: Hard upper bound for the comfort band. ℹ️ Only used when Units='auto' with imperial sensors, or Units='f'. Ignored if using °C. default: 82.0 selector: number: min: 75.0 max: 95.0 step: 0.5 unit_of_measurement: °F mode: slider use_operative_temperature: name: Use Operative Temperature description: 'If on and MRT provided: target uses operative temp (average of air and radiant).' default: false selector: boolean: {} air_velocity: name: Typical Air Velocity (m/s) description: Air movement increases perceived cooling. Values >0.3 m/s activate cooling offsets above 25°C. default: 0.1 selector: number: min: 0.0 max: 2.0 step: 0.1 unit_of_measurement: m/s mode: slider adaptive_air_velocity: name: Auto Fan Adjustment description: If enabled, increases fan speed as the room deviates from the comfort band. default: true selector: boolean: {} humidity_comfort_enable: name: Humidity Comfort Correction description: Applies small offsets for very high (>60%) or very low (<30%) indoor RH. default: true selector: boolean: {} comfort_precision_mode: name: Precision Comfort (more responsive) description: Includes velocity/humidity offsets in the adaptive setpoint. More responsive, slightly chattier. default: false selector: boolean: {} seasons: name: Seasonal Bias & Regional Presets icon: mdi:weather-partly-snowy-rainy collapsed: true input: season_entity: name: Season Entity description: If empty, we infer season from month. Use sensor.season or your own season sensor if available. default: sensor.none selector: entity: domain: - sensor multiple: false reorder: false regional_defaults_enable: name: Enable Regional Presets description: Apply regional defaults (by U.S. state/region) for seasonal bias, ventilation threshold, psychrometrics, and CO₂. default: false selector: boolean: {} user_state_code: name: Region (U.S. State or International) description: Select your region for automatic climate presets. Includes U.S. states and international regions. default: (none) selector: select: options: - (none) - AL - Alabama - AK - Alaska - AZ - Arizona - AR - Arkansas - CA - California - CO - Colorado - CT - Connecticut - DE - Delaware - DC - District of Columbia - FL - Florida - GA - Georgia - HI - Hawaii - ID - Idaho - IL - Illinois - IN - Indiana - IA - Iowa - KS - Kansas - KY - Kentucky - LA - Louisiana - ME - Maine - MD - Maryland - MA - Massachusetts - MI - Michigan - MN - Minnesota - MS - Mississippi - MO - Missouri - MT - Montana - NE - Nebraska - NV - Nevada - NH - New Hampshire - NJ - New Jersey - NM - New Mexico - NY - New York - NC - North Carolina - ND - North Dakota - OH - Ohio - OK - Oklahoma - OR - Oregon - PA - Pennsylvania - RI - Rhode Island - SC - South Carolina - SD - South Dakota - TN - Tennessee - TX - Texas - UT - Utah - VT - Vermont - VA - Virginia - WA - Washington - WV - West Virginia - WI - Wisconsin - WY - Wyoming - CA-AB - Alberta - CA-BC - British Columbia - CA-MB - Manitoba - CA-NB - New Brunswick - CA-NL - Newfoundland and Labrador - CA-NS - Nova Scotia - CA-NT - Northwest Territories - CA-NU - Nunavut - CA-ON - Ontario - CA-PE - Prince Edward Island - CA-QC - Quebec - CA-SK - Saskatchewan - CA-YT - Yukon - UK-ENG - England - UK-SCT - Scotland - UK-WLS - Wales - UK-NIR - Northern Ireland - IE - Ireland - AU-ACT - Australian Capital Territory - AU-NSW - New South Wales - AU-NT - Northern Territory - AU-QLD - Queensland - AU-SA - South Australia - AU-TAS - Tasmania - AU-VIC - Victoria - AU-WA - Western Australia - NZ-NI - North Island, New Zealand - NZ-SI - South Island, New Zealand - AT - Austria - BE - Belgium - CH - Switzerland - CZ - Czech Republic - DE - Germany - DK - Denmark - ES - Spain - FI - Finland - FR - France - GR - Greece - IT - Italy - NL - Netherlands - NO - Norway - PL - Poland - PT - Portugal - SE - Sweden - AE - United Arab Emirates - IL - Israel - IN-N - Northern India - IN-S - Southern India - JP - Japan - KR - South Korea - SA - Saudi Arabia - SG - Singapore - EG - Egypt - ZA - South Africa - AR - Argentina - BR-S - Southern Brazil - BR-SE - Southeast Brazil - CL - Chile - MX - Mexico custom_value: false multiple: false sort: false seasonal_bias_preset: name: Seasonal Bias Preset description: Choose a preset or 'Auto from Region' (uses selected region) or 'None (Manual)' to use your numeric biases. default: Auto from State selector: select: options: - Auto from State - Hot-Humid - Hot-Dry - Marine - Mixed-Humid - Mixed-Dry - Cold - Very Cold - Subarctic - None (Manual) custom_value: false multiple: false sort: false bias_preset_intensity: name: Preset Intensity (x) description: Scales preset magnitudes. 1.0 = recommended; raise if you want stronger seasonal nudges. default: 1.0 selector: number: min: 0.0 max: 2.0 step: 0.1 mode: slider winter_bias: name: Winter Bias (system units) description: 'Manual seasonal nudge when it''s winter: + warms target, - cools it. Ignored if Regional Presets are enabled.' default: 0.0 selector: number: min: -5.0 max: 5.0 step: 0.1 mode: slider summer_bias: name: Summer Bias (system units) description: 'Manual nudge in summer: + warms target, - cools it. Ignored if Regional Presets are enabled.' default: 0.0 selector: number: min: -5.0 max: 5.0 step: 0.1 mode: slider shoulder_bias: name: Spring/Autumn Bias (system units) description: Manual nudge in shoulder seasons. Ignored if Regional Presets are enabled. default: 0.0 selector: number: min: -5.0 max: 5.0 step: 0.1 mode: slider ventilation_energy: name: Ventilation & Energy icon: mdi:leaf collapsed: true input: natural_ventilation_enable: name: Enable Natural Ventilation description: When outdoor conditions are suitable, turn HVAC off and prefer opening windows. default: true selector: boolean: {} natural_ventilation_threshold: name: Temperature Threshold (system units) description: How close outdoor temp must be to indoor to allow natural ventilation (smaller = stricter). default: 2.0 selector: number: min: 0.5 max: 5.0 step: 0.1 mode: slider vent_requires_occupancy: name: Only Prefer Ventilation When Occupied description: If enabled, natural ventilation is only used when occupancy is detected. default: true selector: boolean: {} energy_save_mode: name: Energy Save Mode description: Allows slightly more aggressive setpoints when unoccupied (setback when away). default: true selector: boolean: {} setback_temperature_offset: name: Setback Offset (system units) description: How far we shift the target for energy savings when unoccupied. default: 2.0 selector: number: min: 0.5 max: 5.0 step: 0.1 mode: slider psychrometrics: name: Built-in Psychrometrics icon: mdi:thermometer-water collapsed: true input: psychro_enable: name: Enable Moisture/Economizer Guards description: Uses computed dew point, absolute humidity, and enthalpy to block muggy ventilation and prefer 'free cooling.' default: true selector: boolean: {} max_outdoor_dewpoint_sys: name: Max Outdoor Dew Point (system units) description: Block natural ventilation if outdoor dew point exceeds this (e.g., 65°F / 18.3°C). default: 65.0 selector: number: min: 30.0 max: 80.0 step: 0.5 mode: slider muggy_delta_dp_sys: name: Muggy Delta Dew Point (system units) description: Block natural ventilation if (outdoor_dp - indoor_dp) is greater than or equal to this amount. default: 2.0 selector: number: min: 0.0 max: 10.0 step: 0.5 mode: slider economizer_delta_h: name: Economizer Enthalpy Delta (kJ/kg) description: Prefer ventilation when outdoor enthalpy is lower than indoor by at least this amount. default: 3.0 selector: number: min: 0.0 max: 20.0 step: 0.5 mode: slider economizer_delta_ah: name: Economizer Absolute Humidity Delta (g/m³) description: If enthalpy isn't favorable, still prefer ventilation when AH_out is sufficiently lower than AH_in. default: 2.0 selector: number: min: 0.0 max: 10.0 step: 0.5 mode: slider baro_pressure_sensor: name: Barometric Pressure (optional) description: If provided, overrides sea-level assumption for psychrometrics. Supports kPa, hPa/mbar, Pa, inHg, mmHg. default: sensor.none selector: entity: domain: - sensor multiple: false reorder: false site_elevation_m: name: Site Elevation (m, optional) description: If no pressure sensor, estimate pressure from elevation. Sensor > manual > LUT by state > sea level. default: 0 selector: number: min: -200.0 max: 5000.0 step: 1.0 unit_of_measurement: m mode: slider air_quality_rmot: name: Air Quality & RMOT (Optional) icon: mdi:molecule-co2 collapsed: true input: co2_enable: name: Enable CO₂ Preference description: When indoor CO₂ exceeds outdoor by a threshold, favor ventilation and nudge targets (seasonally). default: false selector: boolean: {} co2_indoor_sensor: name: Indoor CO₂ (ppm) description: Your indoor CO₂ sensor in ppm (optional). default: sensor.none selector: entity: domain: - sensor multiple: false reorder: false co2_outdoor_sensor: name: Outdoor CO₂ (ppm) description: Outdoor (or baseline) CO₂ in ppm (optional). default: sensor.none selector: entity: domain: - sensor multiple: false reorder: false co2_delta_ppm: name: CO₂ Delta Threshold (ppm) description: Treat as ventilation priority when Indoor - Outdoor ≥ this value. default: 400 selector: number: min: 100.0 max: 2000.0 step: 50.0 mode: slider co2_bias_max_sys: name: Max CO₂ Temperature Bias (system units) description: 'Maximum seasonal nudge: warmer in winter, cooler in summer, scaled by CO₂ gap (shoulder = 0).' default: 0.5 selector: number: min: 0.0 max: 3.0 step: 0.1 mode: slider rmot_enable: name: Use Running Mean Outdoor Temperature (RMOT) description: If enabled, provide an RMOT sensor (°C/°F). Replaces outdoor temp in the adaptive model. default: false selector: boolean: {} rmot_sensor: name: RMOT Sensor description: A rolling mean sensor (e.g., 7-14 day average of daily means). Use HA helpers or a Stats sensor. default: sensor.none selector: entity: domain: - sensor - input_number multiple: false reorder: false manual_override: name: Manual Override Detection icon: mdi:hand-back-right collapsed: true input: manual_override_enable: name: Enable Manual Override Detection description: Pause automation when user manually adjusts thermostat, allowing manual control for a period. default: true selector: boolean: {} manual_override_duration_min: name: Override Duration (minutes) description: How long to pause automation after detecting manual adjustment (0 = disabled). default: 60 selector: number: min: 0.0 max: 480.0 step: 5.0 mode: slider manual_override_tolerance: name: Detection Tolerance (climate units) description: Minimum change to count as manual override. Prevents false triggers from small setpoint drift. default: 1.0 selector: number: min: 0.1 max: 5.0 step: 0.1 mode: slider manual_override_action: name: Override Detected Action (optional) description: Actions to run when manual override is detected (notify user, etc.). default: [] selector: action: {} learn_from_overrides: name: Learn from Manual Adjustments description: Gradually adjust comfort model based on your manual temperature changes. Learns your preferences over time. default: true selector: boolean: {} learning_rate: name: Learning Rate description: How quickly to adapt to manual changes. Higher = faster learning but less stable. 0.1 = conservative, 0.3 = aggressive. default: 0.15 selector: number: min: 0.05 max: 0.5 step: 0.05 mode: slider learned_prefs_sensor: name: Learned Preferences Sensor (trigger-based template sensor) description: A trigger-based template sensor that stores learned preferences as JSON. See LEARNING_SETUP.md for setup instructions. This enables time-of-day aware learning with separate heat/cool offsets for day and night. Leave empty to disable learning persistence. default: '' selector: entity: domain: - sensor multiple: false reorder: false manual_override_until: name: Manual Override Until (input_datetime helper) description: Required for override to work correctly. Create an input_datetime helper (date and time) to store when manual override expires. Without this, override will not persist across automation restarts. default: '' selector: entity: domain: - input_datetime multiple: false reorder: false pause: name: HVAC Pause icon: mdi:pause-octagon collapsed: true input: pause_sensors: name: Doors/Windows to Monitor description: Binary sensors that represent openings. HVAC pauses when any are open. default: [] selector: entity: domain: - binary_sensor multiple: true device_class: - door - opening - window reorder: false pause_open_delay: name: Pause after Open (sec) description: Delay before pausing HVAC after an opening turns ON. default: 60 selector: number: min: 0.0 max: 900.0 step: 5.0 mode: slider pause_close_delay: name: Resume after Close (sec) description: Delay before resuming HVAC after all openings are OFF. default: 30 selector: number: min: 0.0 max: 900.0 step: 5.0 mode: slider pause_max_timeout_min: name: Max Pause Timeout (min) description: If > 0, run a timeout action if an opening stays ON beyond this duration. default: 0 selector: number: min: 0.0 max: 240.0 step: 1.0 mode: slider pause_action: name: Pause Action (optional) description: Extra things to do when pausing (notify, lights, etc.). default: [] selector: action: {} accelerate_resume_enable: name: Accelerate Resume Near Risk description: When indoor temp is close to freeze/overheat guards, shorten resume (and optionally open) delays. default: true selector: boolean: {} warn_band_sys: name: Risk Warning Band (system units) description: Range around each guard where acceleration starts (e.g., 3°F / 1.5°C). default: 3.0 selector: number: min: 1.0 max: 10.0 step: 0.5 mode: slider min_resume_delay_sec: name: Minimum Resume Delay (sec) description: Floor for resume delay when accelerating near risk (prevents chatter). default: 5 selector: number: min: 0.0 max: 60.0 step: 1.0 mode: slider min_open_delay_sec: name: Minimum Open Delay (sec) description: Optional floor for open-delay when accelerating (set >0 to also trim open delay near risk). default: 5 selector: number: min: 0.0 max: 60.0 step: 1.0 mode: slider accel_strength: name: Acceleration Strength (0–1.5) description: How aggressively to shorten delays near risk. 0.8 is a good starting point. default: 0.8 selector: number: min: 0.0 max: 1.5 step: 0.1 mode: slider resume_action: name: Resume Action (optional) description: Extra things to do when resuming (notify, restore scene, etc.). default: [] selector: action: {} pause_timeout_action: name: Timeout Action (optional) description: Actions to run if Max Pause Timeout is exceeded while still open. default: [] selector: action: {} safety: name: Safety & Guards icon: mdi:shield-alert collapsed: true input: safety_min_sys: name: Absolute Minimum Temperature (system units) description: Hard floor to prevent freeze risk. Set ≥ 40°F / 4.4°C. All targets and bands will never go below this. default: 40.0 selector: number: min: 35.0 max: 60.0 step: 0.5 mode: slider safety_max_sys: name: Absolute Maximum Temperature (system units) description: Hard ceiling to prevent excessive heat. All targets and bands will never exceed this. default: 92.0 selector: number: min: 80.0 max: 100.0 step: 0.5 mode: slider freeze_protect_enable: name: Freeze-Protect (block HVAC pause when too cold indoors) description: If indoor temp is at/below the freeze guard, treat doors/windows as closed to avoid damaging cold. default: true selector: boolean: {} freeze_guard_sys: name: Freeze Guard Threshold (system units) description: If indoor temp ≤ this, HVAC Pause is disabled (doors/windows ignored). default: 40.0 selector: number: min: 35.0 max: 55.0 step: 0.5 mode: slider overheat_protect_enable: name: Overheat-Protect (block HVAC pause when too hot indoors) description: If indoor temp is at/above the overheat guard, treat doors/windows as closed to allow cooling. default: true selector: boolean: {} overheat_guard_sys: name: Overheat Guard Threshold (system units) description: If indoor temp ≥ this, HVAC Pause is disabled (doors/windows ignored). default: 88.0 selector: number: min: 80.0 max: 95.0 step: 0.5 mode: slider control: name: Control Bands / Debounce icon: mdi:clock-outline collapsed: true input: heat_deadband: name: Heat Deadband (system units) description: Minimum difference between current and target to trigger heating changes. default: 1.5 selector: number: min: 0.5 max: 3.0 step: 0.1 mode: slider cool_deadband: name: Cool Deadband (system units) description: Minimum difference between current and target to trigger cooling changes. default: 1.0 selector: number: min: 0.5 max: 3.0 step: 0.1 mode: slider inside_band_half_width: name: Inside-Band Half-Width (system units) description: Width around the adaptive target when your thermostat supports Heat/Cool (auto). default: 1.0 selector: number: min: 0.5 max: 3.0 step: 0.1 mode: slider auto_min_separation: name: Auto Mode Min Separation (system units) description: If your thermostat requires a minimum gap between low/high in Heat/Cool (Auto), set it here. Leave 0 if not enforced. default: 0.0 selector: number: min: 0.0 max: 5.0 step: 0.1 mode: slider thermostat_profile: name: Thermostat Profile description: Optional vendor profile to enforce a safe minimum separation in Auto/Heat-Cool when the device doesn't report one. 'Auto (detect/device)' prefers the device-advertised minimum if available. default: Auto (detect/device) selector: select: options: - Auto (detect/device) - Ecobee - Google Nest - Honeywell T-series (T5/T6/T9) - Honeywell Home/Resideo (Lyric/Prestige/VisionPRO) - Honeywell T6 Pro Z-Wave - Emerson Sensi - Carrier Infinity/Edge - Bryant Evolution - Trane/American Standard - Lennox iComfort - Bosch Connected Control - Amazon Smart Thermostat - Tado - Netatmo - Hive - Heatmiser Neo - Mysa (Electric Heat) - Wyze - Tuya Zigbee (Generic) - Zigbee (ZHA Generic) - Z-Wave (ZWAVE_JS Generic) - Generic (no minimum) custom_value: false multiple: false sort: false vendor_min_override_sys: name: Override Vendor Min Separation (system units, optional) description: If set (>0), this value overrides the selected profile’s built-in minimum. Use your thermostat’s documented minimum or installer setting. Leave 0 to keep the profile default. default: 0.0 selector: number: min: 0.0 max: 5.0 step: 0.1 mode: slider mode_cooldown_min: name: Min Mode Cooldown (min) description: Optional minimum minutes between switching HVAC modes to reduce short-cycling. default: 10 selector: number: min: 0.0 max: 60.0 step: 1.0 mode: slider sleep: name: Sleep / Circadian Preferences icon: mdi:sleep collapsed: true input: sleep_enable: name: Enable Sleep Cooling description: Apply a nighttime cooling bias and optional tighter band. default: true selector: boolean: {} sleep_mode_entity: name: Sleep Mode (optional) description: Binary sensor or helper that is ON during sleep (e.g., Bedtime, Sleep Mode). Overrides schedule when ON. default: binary_sensor.none selector: entity: domain: - binary_sensor - input_boolean multiple: false reorder: false sleep_start_time: name: Sleep Start description: Time of day sleep usually begins (local). default: '22:00:00' selector: time: {} sleep_end_time: name: Sleep End description: Time of day sleep usually ends (local). default: 07:00:00 selector: time: {} sleep_bias_sys: name: Sleep Bias (system units) description: Shift the adaptive target by this amount during sleep (negative = cooler). default: -1.0 selector: number: min: -5.0 max: 5.0 step: 0.1 mode: slider sleep_tighten_sys: name: Tighten Band (system units) description: Reduce the inside-band half-width during sleep for steadier temps (0 = no change). default: 0.0 selector: number: min: 0.0 max: 3.0 step: 0.1 mode: slider debug: name: Debug icon: mdi:bug-outline collapsed: true input: debug_level: name: Level description: Select 'basic' for helpful trace logs; 'verbose' adds psychrometrics and band calculations. default: basic selector: select: options: - 'off' - basic - verbose custom_value: false multiple: false sort: false variables: blueprint_version: 4.23.5 climate_entity_id: !input climate_entity indoor_sensor_id: !input indoor_temp_sensor outdoor_sensor_id: !input outdoor_temp_sensor _raw_mrt_sensor_values: !input mean_radiant_temp_sensor _raw_rh_in_values: !input indoor_humidity_sensor _raw_rh_out_values: !input outdoor_humidity_sensor _raw_occupancy_values: !input occupancy_sensor _raw_sun_entity_values: !input sun_entity _raw_co2_in_values: !input co2_indoor_sensor _raw_co2_out_values: !input co2_outdoor_sensor _raw_rmot_values: !input rmot_sensor _raw_season_entity_values: !input season_entity units_override_val: !input units_override category: !input comfort_category energy_save: !input energy_save_mode nat_vent_enable: !input natural_ventilation_enable vent_requires_occupancy: !input vent_requires_occupancy dv_sys_in: !input natural_ventilation_threshold setback_sys: !input setback_temperature_offset min_sys_c_pref: !input min_comfort_temp_c max_sys_c_pref: !input max_comfort_temp_c min_sys_f_pref: !input min_comfort_temp_f max_sys_f_pref: !input max_comfort_temp_f use_operative: !input use_operative_temperature v_ms: !input air_velocity adaptive_fan: !input adaptive_air_velocity humid_enable: !input humidity_comfort_enable precision: !input comfort_precision_mode heat_deadband_sys: !input heat_deadband cool_deadband_sys: !input cool_deadband inside_half_sys: !input inside_band_half_width auto_sep_sys: !input auto_min_separation cooldown_min: !input mode_cooldown_min dbg: !input debug_level pause_entities: !input pause_sensors pause_open_delay_in: !input pause_open_delay pause_close_delay_in: !input pause_close_delay pause_timeout_min_val: !input pause_max_timeout_min safety_min_sys_in: !input safety_min_sys safety_max_sys_in: !input safety_max_sys freeze_protect_enable: !input freeze_protect_enable freeze_guard_sys_in: !input freeze_guard_sys overheat_protect_enable: !input overheat_protect_enable overheat_guard_sys_in: !input overheat_guard_sys accelerate_resume_enable: !input accelerate_resume_enable warn_band_sys_in: !input warn_band_sys min_resume_delay_sec_in: !input min_resume_delay_sec min_open_delay_sec_in: !input min_open_delay_sec accel_strength_in: !input accel_strength sleep_enable: !input sleep_enable _raw_sleep_entity_values: !input sleep_mode_entity sleep_start_str: !input sleep_start_time sleep_end_str: !input sleep_end_time sleep_bias_sys_in: !input sleep_bias_sys sleep_tighten_sys_in: !input sleep_tighten_sys manual_override_enable: !input manual_override_enable manual_override_duration_min: !input manual_override_duration_min manual_override_tolerance: !input manual_override_tolerance learn_from_overrides: !input learn_from_overrides learning_rate: !input learning_rate _raw_learned_prefs_sensor: !input learned_prefs_sensor _raw_manual_override_until: !input manual_override_until _u_indoor: '{{ state_attr(indoor_sensor_id, ''unit_of_measurement'') | default('''', true) | string }}' _u_outdoor: "{% if outdoor_sensor_id|string and outdoor_sensor_id.startswith('weather.') %}\n {{ state_attr(outdoor_sensor_id, 'temperature_unit') | default('', true) | string }}\n{% else %}\n {{ state_attr(outdoor_sensor_id, 'unit_of_measurement') | default('', true) | string }}\n{% endif %}\n" _u_climate: '{{ state_attr(climate_entity_id, ''temperature_unit'') | default(state_attr(climate_entity_id, ''unit_of_measurement''), true) | default('''', true) | string }} ' is_imperial: "{% if units_override_val == 'c' %}{{ false }} {% elif units_override_val == 'f' %}{{ true }} {% else %}\n {% set u = (_u_indoor or _u_outdoor or _u_climate) | lower %}\n {{ 'f' in u }}\n{% endif %}\n" _is_weather_in: '{{ indoor_sensor_id is string and indoor_sensor_id.startswith(''weather.'') }}' _is_weather_out: '{{ outdoor_sensor_id is string and outdoor_sensor_id.startswith(''weather.'') }}' unit_sym: '{{ ''°F'' if is_imperial else ''°C'' }}' _to_sys_mult: '{{ 9/5 if is_imperial else 1 }}' _to_sys_add: '{{ 32 if is_imperial else 0 }}' mrt_sensor_id: '{% set raw = _raw_mrt_sensor_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none''] else '''' }} ' rh_in_id: '{% set raw = _raw_rh_in_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none''] else '''' }} ' rh_out_id: '{% set raw = _raw_rh_out_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none'', ''weather.none''] else '''' }} ' occupancy_sensor_id: '{% set raw = _raw_occupancy_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''binary_sensor.none''] else '''' }} ' sun_entity_id: '{% set raw = _raw_sun_entity_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sun.none''] else ''sun.sun'' }} ' sleep_entity_id: '{% set raw = _raw_sleep_entity_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''binary_sensor.none'', ''input_boolean.none''] else '''' }} ' learned_prefs_sensor_id: '{% set raw = _raw_learned_prefs_sensor %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none''] else '''' }} ' _learned_prefs_vars: "{% if learned_prefs_sensor_id %}\n {{ (state_attr(learned_prefs_sensor_id, 'variables') or {}) }}\n{% else %}{{ {} }}{% endif %}\n" _is_night_now: "{% if sun_entity_id %}\n {{ is_state(sun_entity_id, 'below_horizon') }}\n{% else %}\n {# Fallback to sleep schedule if no sun entity #}\n {% set nowm = now().hour * 60 + now().minute %}\n {% set st_parts = (sleep_start_str.split(':') if (sleep_start_str and ':' in sleep_start_str) else ['22','0']) %}\n {% set en_parts = (sleep_end_str.split(':') if (sleep_end_str and ':' in sleep_end_str) else ['7','0']) %}\n {% set s = (st_parts[0] | int(22)) * 60 + (st_parts[1] | int(0)) %}\n {% set e = (en_parts[0] | int(7)) * 60 + (en_parts[1] | int(0)) %}\n {% if s <= e %}{{ (nowm >= s) and (nowm < e) }}\n {% else %}{{ (nowm >= s) or (nowm < e) }}{% endif %}\n{% endif %}\n" _learning_season: "{% set raw = _raw_season_entity_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '') %} {% set v = (e | default('', true) | string | lower) %} {% set season_ent = e if v not in ['', 'none', 'sensor.none'] else '' %} {% if season_ent %}\n {% set s = states(season_ent) | string | lower %}\n {% if s in ['winter'] %}winter\n {% elif s in ['spring'] %}spring\n {% elif s in ['summer'] %}summer\n {% elif s in ['autumn', 'fall'] %}autumn\n {% else %}spring{% endif %}\n{% else %}\n {% set m = now().month %}\n {% if m in [12, 1, 2] %}winter\n {% elif m in [3, 4, 5] %}spring\n {% elif m in [6, 7, 8] %}summer\n {% else %}autumn{% endif %}\n{% endif %}\n" learned_heat_day_winter_sys: '{{ _learned_prefs_vars.get(''heat_day_winter'', 0) | float(0) }}' learned_heat_night_winter_sys: '{{ _learned_prefs_vars.get(''heat_night_winter'', 0) | float(0) }}' learned_cool_day_winter_sys: '{{ _learned_prefs_vars.get(''cool_day_winter'', 0) | float(0) }}' learned_cool_night_winter_sys: '{{ _learned_prefs_vars.get(''cool_night_winter'', 0) | float(0) }}' learned_heat_day_spring_sys: '{{ _learned_prefs_vars.get(''heat_day_spring'', 0) | float(0) }}' learned_heat_night_spring_sys: '{{ _learned_prefs_vars.get(''heat_night_spring'', 0) | float(0) }}' learned_cool_day_spring_sys: '{{ _learned_prefs_vars.get(''cool_day_spring'', 0) | float(0) }}' learned_cool_night_spring_sys: '{{ _learned_prefs_vars.get(''cool_night_spring'', 0) | float(0) }}' learned_heat_day_summer_sys: '{{ _learned_prefs_vars.get(''heat_day_summer'', 0) | float(0) }}' learned_heat_night_summer_sys: '{{ _learned_prefs_vars.get(''heat_night_summer'', 0) | float(0) }}' learned_cool_day_summer_sys: '{{ _learned_prefs_vars.get(''cool_day_summer'', 0) | float(0) }}' learned_cool_night_summer_sys: '{{ _learned_prefs_vars.get(''cool_night_summer'', 0) | float(0) }}' learned_heat_day_autumn_sys: '{{ _learned_prefs_vars.get(''heat_day_autumn'', 0) | float(0) }}' learned_heat_night_autumn_sys: '{{ _learned_prefs_vars.get(''heat_night_autumn'', 0) | float(0) }}' learned_cool_day_autumn_sys: '{{ _learned_prefs_vars.get(''cool_day_autumn'', 0) | float(0) }}' learned_cool_night_autumn_sys: '{{ _learned_prefs_vars.get(''cool_night_autumn'', 0) | float(0) }}' learned_offset_sys: "{% set season = _learning_season %} {% set time = 'night' if _is_night_now else 'day' %} {% if season == 'winter' %}\n {% set heat_val = learned_heat_day_winter_sys if time == 'day' else learned_heat_night_winter_sys %}\n {% set cool_val = learned_cool_day_winter_sys if time == 'day' else learned_cool_night_winter_sys %}\n {# Winter: use heating preference (85/15 weight) #}\n {{ (0.85 * (heat_val | float(0)) + 0.15 * (cool_val | float(0))) | round(2) }}\n{% elif season == 'spring' %}\n {% set heat_val = learned_heat_day_spring_sys if time == 'day' else learned_heat_night_spring_sys %}\n {% set cool_val = learned_cool_day_spring_sys if time == 'day' else learned_cool_night_spring_sys %}\n {# Spring: balanced average #}\n {{ ((heat_val | float(0)) + (cool_val | float(0))) / 2 }}\n{% elif season == 'summer' %}\n {% set heat_val = learned_heat_day_summer_sys if time == 'day' else learned_heat_night_summer_sys %}\n {% set cool_val = learned_cool_day_summer_sys if time == 'day' else learned_cool_night_summer_sys %}\n {# Summer: use cooling preference (15/85 weight) #}\n {{ (0.15 * (heat_val | float(0)) + 0.85 * (cool_val | float(0))) | round(2) }}\n{% else %}\n {% set heat_val = learned_heat_day_autumn_sys if time == 'day' else learned_heat_night_autumn_sys %}\n {% set cool_val = learned_cool_day_autumn_sys if time == 'day' else learned_cool_night_autumn_sys %}\n {# Autumn: slight heating bias (60/40 weight) #}\n {{ (0.6 * (heat_val | float(0)) + 0.4 * (cool_val | float(0))) | round(2) }}\n{% endif %}\n" learned_offset_c: '{{ (learned_offset_sys * 5/9) if is_imperial else learned_offset_sys }}' manual_override_until_id: '{% set raw = _raw_manual_override_until %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''input_datetime.none''] else '''' }} ' is_override_active: "{% if not manual_override_enable or manual_override_duration_min | int(0) <= 0 or not manual_override_until_id %}\n {{ false }}\n{% else %}\n \ {% set until_ts = state_attr(manual_override_until_id, 'timestamp') %}\n {% set now_ts = as_timestamp(now()) %}\n {% if until_ts is none or now_ts is none %}\n {{ false }}\n {% else %}\n {{ now_ts < until_ts }}\n {% endif %}\n{% endif %}\n" regional_defaults_enable: !input regional_defaults_enable state_code: !input user_state_code bias_preset: !input seasonal_bias_preset bias_intensity_raw: !input bias_preset_intensity psychro_enable: !input psychro_enable max_outdoor_dp_sys_in: !input max_outdoor_dewpoint_sys muggy_delta_dp_sys_in: !input muggy_delta_dp_sys econ_dh_in: !input economizer_delta_h econ_dah_in: !input economizer_delta_ah co2_enable: !input co2_enable co2_delta_ppm_in: !input co2_delta_ppm co2_bias_max_sys: !input co2_bias_max_sys rmot_enable: !input rmot_enable co2_in_id: '{% set raw = _raw_co2_in_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none''] else '''' }} ' co2_out_id: '{% set raw = _raw_co2_out_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none''] else '''' }} ' rmot_sensor_id: '{% set raw = _raw_rmot_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none'', ''input_number.none''] else '''' }} ' _raw_baro_values: !input baro_pressure_sensor baro_sensor_id: '{% set raw = _raw_baro_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none''] else '''' }} ' season_entity_id: '{% set raw = _raw_season_entity_values %} {% set e = raw if raw is string else (raw[0] if (raw is iterable and raw|length > 0) else '''') %} {% set v = (e | default('''', true) | string | lower) %} {{ e if v not in ['''', ''none'', ''sensor.none''] else '''' }} ' winter_bias_sys_manual: !input winter_bias summer_bias_sys_manual: !input summer_bias shoulder_bias_sys_manual: !input shoulder_bias bias_intensity: '{{ bias_intensity_raw | float(1.0) }}' safety_min_c: '{{ ((safety_min_sys_in|float - 32) * 5/9) if is_imperial else (safety_min_sys_in|float) }}' safety_max_c: '{{ ((safety_max_sys_in|float - 32) * 5/9) if is_imperial else (safety_max_sys_in|float) }}' freeze_guard_c: '{{ ((freeze_guard_sys_in|float - 32) * 5/9) if is_imperial else (freeze_guard_sys_in|float) }}' overheat_guard_c: '{{ ((overheat_guard_sys_in|float - 32) * 5/9) if is_imperial else (overheat_guard_sys_in|float) }}' t_in_sys: '{% set v = (state_attr(indoor_sensor_id,''temperature'') if _is_weather_in else states(indoor_sensor_id)) | float(20) %} {% set u = (state_attr(indoor_sensor_id,''temperature_unit'') if _is_weather_in else state_attr(indoor_sensor_id,''unit_of_measurement'')) | default('''', true) | string %} {% set c = ((v - 32) * 5 / 9) if (u|lower in [''°f'',''f'',''fahrenheit''] or (u == '''' and is_imperial)) else v %} {{ (c * 9/5 + 32) if is_imperial else c }} ' warn_band_sys: '{{ warn_band_sys_in | float(3.0) }}' _risk_cold: '{{ [ (freeze_guard_sys_in | float(45)) - (t_in_sys | float(0)), 0 ] | max }}' _risk_hot: '{{ [ (t_in_sys | float(0)) - (overheat_guard_sys_in | float(88)), 0 ] | max }}' risk_near: '{% set wb = warn_band_sys | float(3.0) %} {% set r = ([ _risk_cold | float(0), _risk_hot | float(0) ] | max) %} {% if wb <= 0 %} 0.0 {% else %} {{ [ (r / wb), 1.0 ] | min }} {% endif %} ' risk_active: '{{ (accelerate_resume_enable | bool) and (risk_near | float(0)) > 0 and not ((t_in_sys | float(0)) <= (freeze_guard_sys_in | float(45)) or (t_in_sys | float(0)) >= (overheat_guard_sys_in | float(88))) }} ' safety_reenable_freeze_needed: '{% set wb = warn_band_sys | float(3.0) %} {% set rc = _risk_cold | float(0) %} {% set r = 0.0 if wb <= 0 else (rc / wb) %} {{ freeze_protect_enable and (r >= 0.7) }} ' safety_reenable_overheat_needed: '{% set wb = warn_band_sys | float(3.0) %} {% set rh = _risk_hot | float(0) %} {% set r = 0.0 if wb <= 0 else (rh / wb) %} {{ overheat_protect_enable and (r >= 0.7) }} ' safety_reenable_needed: '{{ safety_reenable_freeze_needed or safety_reenable_overheat_needed }}' pause_open_delay_base: '{{ pause_open_delay_in | int(0) }}' pause_close_delay_base: '{{ pause_close_delay_in | int(0) }}' pause_close_delay_eff: "{% set base = pause_close_delay_base | int(0) %} {% if risk_active %}\n {% set k = accel_strength_in | float(0.8) %}\n {% set scaled = (base * (1 - (k * (risk_near | float(0))))) | round(0) %}\n {{ [ min_resume_delay_sec_in | int(5), scaled | int(0) ] | max }}\n{% else %}\n {{ base }}\n{% endif %}\n" pause_open_delay_eff: "{% set base = pause_open_delay_base | int(0) %} {% if risk_active %}\n {# Use 50% of acceleration strength for open delay (less aggressive than resume) #}\n {% set k = (0.5 * (accel_strength_in | float(0.8))) %}\n {% set scaled = (base * (1 - (k * (risk_near | float(0))))) | round(0) %}\n {{ [ min_open_delay_sec_in | int(5), scaled | int(0) ] | max }}\n{% else %}\n {{ base }}\n{% endif %}\n" min_sys: '{{ min_sys_f_pref if is_imperial else min_sys_c_pref }}' max_sys: '{{ max_sys_f_pref if is_imperial else max_sys_c_pref }}' dv_sys: "{# Effective ventilation threshold after regional preset #} {% set base = dv_sys_in | float(2.0) %} {% if regional_defaults_enable %}\n {% set r = (state_code.split(' ')[0] if state_code else '') %}\n {% set tight_states = ['AZ','NV','TX','FL','LA','MS','AL','GA','SC'] %}\n {% set loose_states = ['CA','OR','WA','CO','UT','NM'] %}\n {% if r in tight_states %}\n {{ 1.5 }}\n {% elif r in loose_states %}\n {{ 2.0 }}\n {% else %}\n \ {{ base }}\n {% endif %}\n{% else %}\n {{ base }}\n{% endif %}\n" setback_c: '{{ (setback_sys|float * 5/9) if is_imperial else (setback_sys|float) }}' min_c: '{{ [ ((min_sys|float - 32) * 5/9) if is_imperial else (min_sys|float), safety_min_c ] | max }}' max_c: '{{ [ ((max_sys|float - 32) * 5/9) if is_imperial else (max_sys|float), safety_max_c ] | min }}' inside_half_c: '{{ (inside_half_sys|float * 5/9) if is_imperial else (inside_half_sys|float) }}' _region_lut: "{{ {\n 'AL': 'Hot-Humid', 'AK': 'Very Cold', 'AZ': 'Hot-Dry', 'AR': 'Mixed-Humid',\n 'CA': 'Marine', 'CO': 'Mixed-Dry', 'CT': 'Cold', 'DE': 'Mixed-Humid',\n \ 'DC': 'Mixed-Humid', 'FL': 'Hot-Humid', 'GA': 'Hot-Humid', 'HI': 'Hot-Humid',\n \ 'ID': 'Mixed-Dry', 'IL': 'Mixed-Humid', 'IN': 'Mixed-Humid', 'IA': 'Cold',\n \ 'KS': 'Mixed-Humid', 'KY': 'Mixed-Humid', 'LA': 'Hot-Humid', 'ME': 'Cold',\n \ 'MD': 'Mixed-Humid', 'MA': 'Cold', 'MI': 'Cold', 'MN': 'Cold',\n 'MS': 'Hot-Humid', 'MO': 'Mixed-Humid', 'MT': 'Mixed-Dry', 'NE': 'Mixed-Dry',\n 'NV': 'Hot-Dry', 'NH': 'Cold', 'NJ': 'Mixed-Humid', 'NM': 'Hot-Dry',\n 'NY': 'Cold', 'NC': 'Mixed-Humid', 'ND': 'Mixed-Dry', 'OH': 'Mixed-Humid',\n 'OK': 'Mixed-Humid', 'OR': 'Marine', 'PA': 'Mixed-Humid', 'RI': 'Cold',\n 'SC': 'Hot-Humid', 'SD': 'Mixed-Dry', 'TN': 'Mixed-Humid', 'TX': 'Hot-Humid',\n 'UT': 'Hot-Dry', 'VT': 'Cold', 'VA': 'Mixed-Humid', 'WA': 'Marine',\n 'WV': 'Mixed-Humid', 'WI': 'Cold', 'WY': 'Mixed-Dry',\n 'CA-AB': 'Very Cold', 'CA-BC': 'Marine', 'CA-MB': 'Very Cold', 'CA-NB': 'Cold',\n 'CA-NL': 'Cold', 'CA-NS': 'Cold', 'CA-NT': 'Subarctic', 'CA-NU': 'Subarctic',\n 'CA-ON': 'Cold', 'CA-PE': 'Cold', 'CA-QC': 'Cold', 'CA-SK': 'Very Cold', 'CA-YT': 'Subarctic',\n \ 'UK-ENG': 'Marine', 'UK-SCT': 'Cold', 'UK-WLS': 'Marine', 'UK-NIR': 'Marine', 'IE': 'Marine',\n 'AU-NSW': 'Mixed-Humid', 'AU-VIC': 'Marine', 'AU-QLD': 'Hot-Humid', 'AU-WA': 'Hot-Dry',\n 'AU-SA': 'Hot-Dry', 'AU-TAS': 'Marine', 'AU-NT': 'Hot-Humid', 'AU-ACT': 'Mixed-Dry',\n 'NZ-NI': 'Marine', 'NZ-SI': 'Marine',\n 'DE': 'Cold', 'FR': 'Marine', 'ES': 'Mixed-Dry', 'IT': 'Mixed-Humid',\n 'NL': 'Marine', 'BE': 'Marine', 'AT': 'Cold', 'CH': 'Cold',\n 'SE': 'Cold', 'NO': 'Very Cold', 'DK': 'Cold', 'FI': 'Very Cold',\n 'PL': 'Cold', 'CZ': 'Cold', 'PT': 'Marine', 'GR': 'Mixed-Dry',\n 'JP': 'Mixed-Humid', 'KR': 'Mixed-Humid', 'SG': 'Hot-Humid',\n \ 'IN-N': 'Mixed-Dry', 'IN-S': 'Hot-Humid',\n 'ZA': 'Mixed-Dry', 'EG': 'Hot-Dry', 'AE': 'Hot-Dry', 'SA': 'Hot-Dry', 'IL': 'Hot-Dry',\n 'MX': 'Hot-Dry', 'BR-SE': 'Hot-Humid', 'BR-S': 'Mixed-Humid', 'AR': 'Mixed-Humid', 'CL': 'Marine'\n} }}\n" _region_from_state: '{% set s = (state_code.split('' '')[0] if state_code else '''') %} {% set r = _region_lut.get(s, ''None'') %} {{ r if r != ''None'' else ''None'' }} ' region_pick: '{% if not regional_defaults_enable %} None {% elif bias_preset == ''Auto from State'' %} {{ _region_from_state }} {% elif bias_preset == ''None (Manual)'' %} None {% else %} {{ bias_preset }} {% endif %} ' _dp_max_c: '{% set r = region_pick %} {% if r == ''Hot-Humid'' %} 18.0 {% elif r == ''Hot-Dry'' %} 19.0 {% elif r == ''Marine'' %} 16.5 {% elif r == ''Mixed-Humid'' %} 17.0 {% elif r == ''Mixed-Dry'' %} 16.0 {% elif r == ''Cold'' %} 17.0 {% elif r == ''Very Cold'' %} 16.0 {% elif r == ''Subarctic'' %} 15.0 {% else %} {{ none }} {% endif %} ' _muggy_delta_c: '{% set r = region_pick %} {% if r == ''Hot-Humid'' %} 1.0 {% elif r == ''Hot-Dry'' %} 2.0 {% elif r == ''Marine'' %} 1.5 {% elif r == ''Mixed-Humid'' %} 1.5 {% elif r == ''Mixed-Dry'' %} 1.5 {% elif r == ''Cold'' %} 1.5 {% elif r == ''Very Cold'' %} 1.5 {% elif r == ''Subarctic'' %} 2.0 {% else %} {{ none }} {% endif %} ' _econ_dh: '{% set r = region_pick %} {% if r == ''Hot-Humid'' %} 4.0 {% elif r == ''Hot-Dry'' %} 2.0 {% elif r == ''Marine'' %} 3.0 {% elif r == ''Mixed-Humid'' %} 3.5 {% elif r == ''Mixed-Dry'' %} 2.5 {% elif r == ''Cold'' %} 3.0 {% elif r == ''Very Cold'' %} 3.0 {% elif r == ''Subarctic'' %} 2.5 {% else %} {{ none }} {% endif %} ' _econ_dah: '{% set r = region_pick %} {% if r == ''Hot-Humid'' %} 3.0 {% elif r == ''Hot-Dry'' %} 1.0 {% elif r == ''Marine'' %} 2.0 {% elif r == ''Mixed-Humid'' %} 2.0 {% elif r == ''Mixed-Dry'' %} 1.5 {% elif r == ''Cold'' %} 2.0 {% elif r == ''Very Cold'' %} 2.0 {% elif r == ''Subarctic'' %} 1.5 {% else %} {{ none }} {% endif %} ' _co2_delta: '{% set r = region_pick %} {% if r == ''Hot-Humid'' %} 450 {% elif r == ''Hot-Dry'' %} 450 {% elif r == ''Marine'' %} 400 {% elif r == ''Mixed-Humid'' %} 450 {% elif r == ''Mixed-Dry'' %} 450 {% elif r == ''Cold'' %} 450 {% elif r == ''Very Cold'' %} 500 {% elif r == ''Subarctic'' %} 550 {% else %} {{ none }} {% endif %} ' max_outdoor_dp_sys: "{% if _dp_max_c is not none %}\n {{ (_dp_max_c * 9/5 + 32) if is_imperial else _dp_max_c }}\n{% else %} {{ max_outdoor_dp_sys_in }} {% endif %}\n" muggy_delta_dp_sys: "{% if _muggy_delta_c is not none %}\n {{ (_muggy_delta_c * 9/5) if is_imperial else _muggy_delta_c }}\n{% else %} {{ muggy_delta_dp_sys_in }} {% endif %}\n" econ_dh: '{{ _econ_dh if _econ_dh is not none else econ_dh_in }}' econ_dah: '{{ _econ_dah if _econ_dah is not none else econ_dah_in }}' co2_delta_ppm: '{{ _co2_delta if _co2_delta is not none else co2_delta_ppm_in }}' max_outdoor_dp_c: '{{ ((max_outdoor_dp_sys|float - 32) * 5/9) if is_imperial else (max_outdoor_dp_sys|float) }}' muggy_delta_dp_c: '{{ (muggy_delta_dp_sys|float * 5/9) if is_imperial else (muggy_delta_dp_sys|float) }}' co2_bias_max_c: '{{ (co2_bias_max_sys|float * 5/9) if is_imperial else (co2_bias_max_sys|float) }}' tol_c: '{% if category == ''I'' %}2.0{% elif category == ''II'' %}3.0{% else %}4.0{% endif %}' t_out_c_raw: '{% set v = (state_attr(outdoor_sensor_id,''temperature'') if _is_weather_out else states(outdoor_sensor_id)) | float(20) %} {% set u = (state_attr(outdoor_sensor_id,''temperature_unit'') if _is_weather_out else state_attr(outdoor_sensor_id,''unit_of_measurement'')) | default('''', true) | string %} {% if u|lower in [''°f'',''f'',''fahrenheit''] or (u == '''' and is_imperial) %} {{ ((v - 32) * 5 / 9) }} {% else %} {{ v }} {% endif %} ' t_rmot_c: "{# Robust RMOT parsing: skip if unknown/unavailable, handle units #} {% if rmot_enable and rmot_sensor_id %}\n {% set raw = states(rmot_sensor_id) %}\n {% set v = (raw if raw not in ['unknown','unavailable',''] else none) %}\n \ {% if v is not none %}\n {% set vf = v | float(none) %}\n {% set u = state_attr(rmot_sensor_id, 'unit_of_measurement') | default('', true) | string %}\n {% if vf is number %}\n {% if u|lower in ['°f','f','fahrenheit'] or (u == '' and is_imperial) %} {{ ((vf - 32) * 5 / 9) }}\n {% else %} {{ vf }} {% endif %}\n {% else %}\n {{ none }}\n {% endif %}\n {% else %}\n {{ none }}\n {% endif %}\n{% else %} {{ none }} {% endif %}\n" t_out_c: '{{ t_rmot_c if t_rmot_c is not none else t_out_c_raw }}' t_in_air_c: '{% set v = (state_attr(indoor_sensor_id,''temperature'') if _is_weather_in else states(indoor_sensor_id)) | float(20) %} {% set u = (state_attr(indoor_sensor_id,''temperature_unit'') if _is_weather_in else state_attr(indoor_sensor_id,''unit_of_measurement'')) | default('''', true) | string %} {% if u|lower in [''°f'',''f'',''fahrenheit''] or (u == '''' and is_imperial) %} {{ ((v - 32) * 5 / 9) }} {% else %} {{ v }} {% endif %} ' t_mrt_c: "{% if mrt_sensor_id %}\n {% set v = (state_attr(mrt_sensor_id,'temperature') if _is_weather_in else states(mrt_sensor_id)) | float(t_in_air_c) %}\n {% set u = (state_attr(mrt_sensor_id,'temperature_unit') if _is_weather_in else state_attr(mrt_sensor_id,'unit_of_measurement')) | default('', true) | string %}\n {% if u|lower in ['°f','f','fahrenheit'] or (u == '' and is_imperial) %} {{ ((v - 32) * 5 / 9) }}\n {% else %} {{ v }} {% endif %}\n{% else %} {{ t_in_air_c }} {% endif %}\n" t_in_c: '{% if use_operative and mrt_sensor_id %} {{ ((t_in_air_c + t_mrt_c) | float / 2) | round(2) }} {% else %} {{ t_in_air_c }} {% endif %} ' rh_in: '{% if rh_in_id %}{{ states(rh_in_id) | float(50) }}{% else %}50{% endif %}' rh_out: "{% if rh_out_id %}\n {% if _is_weather_out %}\n {{ state_attr(rh_out_id, 'humidity') | float(50) }}\n {% elif state_attr(rh_out_id, 'humidity') is not none %}\n {{ state_attr(rh_out_id, 'humidity') | float(50) }}\n {% else %}\n \ {{ states(rh_out_id) | float(50) }}\n {% endif %}\n{% else %}50{% endif %}\n" _state_elev_lut: "{{ {\n 'AL':150,'AK':580,'AZ':1250,'AR':200,'CA':880,'CO':2000,'CT':150,'DC':20,'DE':20,'FL':30,\n \ 'GA':180,'HI':920,'ID':1520,'IL':180,'IN':210,'IA':335,'KS':610,'KY':230,'LA':30,'ME':180,\n \ 'MD':110,'MA':150,'MI':270,'MN':370,'MS':90,'MO':240,'MT':1035,'NE':790,'NV':1675,'NH':300,\n \ 'NJ':80,'NM':1735,'NY':300,'NC':220,'ND':580,'OH':260,'OK':400,'OR':1000,'PA':340,'RI':60,\n \ 'SC':110,'SD':670,'TN':260,'TX':520,'UT':1890,'VT':300,'VA':290,'WA':520,'WV':460,'WI':320,'WY':2040,\n \ 'CA-AB':1045,'CA-BC':500,'CA-MB':240,'CA-NB':130,'CA-NL':200,'CA-NS':100,'CA-NT':200,'CA-NU':30,\n \ 'CA-ON':260,'CA-PE':50,'CA-QC':180,'CA-SK':520,'CA-YT':660,\n 'UK-ENG':100,'UK-SCT':200,'UK-WLS':150,'UK-NIR':80,'IE':120,\n \ 'AU-NSW':300,'AU-VIC':200,'AU-QLD':260,'AU-WA':350,'AU-SA':200,'AU-TAS':300,'AU-NT':350,'AU-ACT':580,\n \ 'NZ-NI':200,'NZ-SI':400,\n 'DE':260,'FR':300,'ES':660,'IT':420,'NL':10,'BE':180,'AT':910,'CH':1350,\n \ 'SE':320,'NO':460,'DK':30,'FI':160,'PL':175,'CZ':430,'PT':370,'GR':500,\n 'JP':400,'KR':280,'SG':15,'IN-N':240,'IN-S':500,\n \ 'ZA':1200,'EG':100,'AE':50,'SA':600,'IL':500,\n 'MX':1100,'BR-SE':750,'BR-S':500,'AR':400,'CL':570\n} }}\n" _state_code: '{{ (state_code.split('' '')[0] if state_code else '''') }}' lut_elev_m: '{% set d = _state_elev_lut %} {% if d is mapping and _state_code in d %}{{ d[_state_code] }}{% else %}0{% endif %} ' site_elevation_m_in: !input site_elevation_m site_elevation_eff_m: '{# prefer manual elevation if not zero/non-default, else LUT when regional presets enabled, else 0 #} {% set man = site_elevation_m_in | float(0) %} {% if man != 0 %}{{ man }} {% elif regional_defaults_enable %}{{ lut_elev_m | float(0) }} {% else %}0{% endif %} ' _baro_unit: '{{ state_attr(baro_sensor_id, ''unit_of_measurement'') | default('''', true) | string }}' _baro_val_raw: '{{ states(baro_sensor_id) if baro_sensor_id else '''' }}' _baro_val_num: "{% if baro_sensor_id and _baro_val_raw not in ['unknown','unavailable',''] %}\n {{ _baro_val_raw | float(0) }}\n{% else %}{{ none }}{% endif %}\n" baro_kpa_from_sensor: "{% set v = _baro_val_num %} {% if v is not none %}\n {% set u = _baro_unit | lower %}\n {% if 'kpa' in u %}{{ v }}\n {% elif 'hpa' in u or 'mbar' in u %}{{ v / 10.0 }}\n {% elif u == 'pa' %}{{ v / 1000.0 }}\n {% elif 'inhg' in u %}{{ v * 3.38639 }}\n {% elif 'mmhg' in u %}{{ v * 0.133322 }}\n {% else %}{{ none }}{% endif %}\n{% else %}{{ none }}{% endif %}\n" baro_kpa_from_elev: "{% set h = site_elevation_eff_m | float(0) %} {% if h > -200 %}\n {{ 101.325 * (1 - 2.25577e-5 * h) ** 5.25588 }}\n{% else %}{{ 101.325 }}{% endif %}\n" pressure_kpa: '{% if baro_kpa_from_sensor is not none %}{{ baro_kpa_from_sensor }} {% else %}{{ baro_kpa_from_elev }}{% endif %} ' _psychro_on: '{{ psychro_enable or dbg == ''verbose'' }}' psat_in_kpa: '{% if _psychro_on and t_in_c is number %}{{ 0.61078 * (2.718281828 ** ((17.2694 * t_in_c) / (t_in_c + 237.3))) }}{% else %}{{ none }}{% endif %} ' psat_out_kpa: '{% if _psychro_on and t_out_c is number %}{{ 0.61078 * (2.718281828 ** ((17.2694 * t_out_c) / (t_out_c + 237.3))) }}{% else %}{{ none }}{% endif %} ' pv_in_kpa: '{{ ( (rh_in | float(50) / 100.0) * psat_in_kpa ) if (_psychro_on and psat_in_kpa is number) else none }}' pv_out_kpa: '{{ ( (rh_out | float(50) / 100.0) * psat_out_kpa ) if (_psychro_on and psat_out_kpa is number) else none }}' dp_in_c: "{% if _psychro_on and pv_in_kpa is number and pv_in_kpa | float(0) > 0.001 %}\n {% set x = pv_in_kpa / 0.61078 %}\n {% if x > 0 %}{% set ln = log(x) %}{{ (237.3 * ln) / (17.2694 - ln) }}{% else %}{{ none }}{% endif %}\n{% else %}{{ none }}{% endif %}\n" dp_out_c: "{% if _psychro_on and pv_out_kpa is number and pv_out_kpa | float(0) > 0.001 %}\n {% set x = pv_out_kpa / 0.61078 %}\n {% if x > 0 %}{% set ln = log(x) %}{{ (237.3 * ln) / (17.2694 - ln) }}{% else %}{{ none }}{% endif %}\n{% else %}{{ none }}{% endif %}\n" ah_in: '{% if _psychro_on and t_in_c is number and pv_in_kpa is number %}{% set Tk = t_in_c + 273.15 %}{{ ((2.1674 * pv_in_kpa / Tk) * 1000.0) | round(2) }}{% else %}{{ none }}{% endif %} ' ah_out: '{% if _psychro_on and t_out_c is number and pv_out_kpa is number %}{% set Tk = t_out_c + 273.15 %}{{ ((2.1674 * pv_out_kpa / Tk) * 1000.0) | round(2) }}{% else %}{{ none }}{% endif %} ' w_in: '{{ (0.62198 * pv_in_kpa / (pressure_kpa - pv_in_kpa)) if (_psychro_on and pressure_kpa is number and pv_in_kpa is number and (pressure_kpa - pv_in_kpa) > 0.1) else none }}' w_out: '{{ (0.62198 * pv_out_kpa / (pressure_kpa - pv_out_kpa)) if (_psychro_on and pressure_kpa is number and pv_out_kpa is number and (pressure_kpa - pv_out_kpa) > 0.1) else none }}' h_in: '{{ ((1.006 * t_in_c + w_in * (2501 + 1.86 * t_in_c)) | round(2)) if (_psychro_on and w_in is number) else none }}' h_out: '{{ ((1.006 * t_out_c + w_out * (2501 + 1.86 * t_out_c)) | round(2)) if (_psychro_on and w_out is number) else none }}' occ_now: "{% if occupancy_sensor_id %}\n {% set raw = states(occupancy_sensor_id) | default('', true) | string %}\n {% set s = raw | lower %}\n {% set dc = state_attr(occupancy_sensor_id, 'device_class') | default('', true) | string | lower %}\n {# Normalize common truthy/falsey and presence states #}\n {% set on_states = ['on','home','present','occupied','true','detected','motion'] %}\n {% set off_states = ['off','not_home','away','clear','false','standby','none','closed'] %}\n {% if s in on_states %}\n {{ true }}\n {% elif s in off_states %}\n \ {{ false }}\n {% else %}\n {# Fallbacks by device class: presence/occupancy/motion default false unless explicitly on; person/device_tracker: home=on, not_home=off #}\n {% if dc in ['presence','occupancy','motion'] %}\n {{ s in on_states }}\n {% else %}\n {# Numeric or unknown states: treat >0 as on, else keep previous truthy if possible #}\n {% if s|float(none) is not none %}\n {{ (s | float(0)) > 0 }}\n {% else %}\n {{ false }}\n {% endif %}\n \ {% endif %}\n {% endif %}\n{% else %}\n {{ true }}\n{% endif %}\n" setback_apply_c: '{{ setback_c if (energy_save and (not (occ_now | default(false) | bool))) else 0 }}' sleep_bias_c: '{{ (sleep_bias_sys_in|float * 5/9) if is_imperial else (sleep_bias_sys_in|float) }}' sleep_tighten_c: '{{ (sleep_tighten_sys_in|float * 5/9) if is_imperial else (sleep_tighten_sys_in|float) }}' _time_active: '{% set nowm = now().hour * 60 + now().minute %} {% set st_parts = (sleep_start_str.split('':'') if (sleep_start_str and '':'' in sleep_start_str) else [''22'',''0'']) %} {% set en_parts = (sleep_end_str.split('':'') if (sleep_end_str and '':'' in sleep_end_str) else [''7'',''0'']) %} {% set s = (st_parts[0] | int(22)) * 60 + (st_parts[1] | int(0)) %} {% set e = (en_parts[0] | int(7)) * 60 + (en_parts[1] | int(0)) %} {% if s <= e %}{{ (nowm >= s) and (nowm < e) }} {% else %}{{ (nowm >= s) or (nowm < e) }}{% endif %} ' _sleep_switch: '{% if sleep_entity_id %}{{ is_state(sleep_entity_id, ''on'') }}{% else %}false{% endif %} ' sleep_active: '{{ sleep_enable and ( (_sleep_switch|bool) or (_time_active|bool) ) and (occ_now|bool) }} ' sleep_bias_c_eff: '{{ sleep_bias_c if sleep_active else 0 }}' inside_half_c_eff: "{% if sleep_active %}\n {% set narrowed = (inside_half_c - sleep_tighten_c) %}\n {{ [narrowed, 0.4] | max }}\n{% else %}{{ inside_half_c }}{% endif %}\n" _region_for_bias: '{{ region_pick }}' _preset_w_c: '{% set r = _region_for_bias %} {% set intensity = bias_intensity | float(1.0) %} {% if r == ''Subarctic'' %} {{ 0.8 * intensity }} {% elif r == ''Very Cold'' %} {{ 0.6 * intensity }} {% elif r == ''Cold'' %} {{ 0.5 * intensity }} {% elif r == ''Mixed-Humid'' %} {{ 0.3 * intensity }} {% elif r == ''Mixed-Dry'' %} {{ 0.3 * intensity }} {% elif r == ''Hot-Humid'' %} {{ 0.1 * intensity }} {% elif r == ''Hot-Dry'' %} {{ 0.0 * intensity }} {% elif r == ''Marine'' %} {{ 0.1 * intensity }} {% else %} {{ none }} {% endif %} ' _preset_s_c: '{% set r = _region_for_bias %} {% set intensity = bias_intensity | float(1.0) %} {% if r == ''Subarctic'' %} {{ 0.0 * intensity }} {% elif r == ''Very Cold'' %} {{ -0.2 * intensity }} {% elif r == ''Cold'' %} {{ -0.2 * intensity }} {% elif r == ''Mixed-Humid'' %} {{ -0.3 * intensity }} {% elif r == ''Mixed-Dry'' %} {{ -0.2 * intensity }} {% elif r == ''Hot-Humid'' %} {{ -0.5 * intensity }} {% elif r == ''Hot-Dry'' %} {{ -0.4 * intensity }} {% elif r == ''Marine'' %} {{ -0.1 * intensity }} {% else %} {{ none }} {% endif %} ' _preset_sh_c: '{% set r = _region_for_bias %} {% set intensity = bias_intensity | float(1.0) %} {% if r == ''Subarctic'' %} {{ 0.4 * intensity }} {% elif r == ''Very Cold'' %} {{ 0.3 * intensity }} {% elif r == ''Cold'' %} {{ 0.2 * intensity }} {% elif r == ''Mixed-Humid'' %} {{ 0.0 * intensity }} {% elif r == ''Mixed-Dry'' %} {{ 0.0 * intensity }} {% elif r == ''Hot-Humid'' %} {{ -0.2 * intensity }} {% elif r == ''Hot-Dry'' %} {{ -0.1 * intensity }} {% elif r == ''Marine'' %} {{ 0.0 * intensity }} {% else %} {{ none }} {% endif %} ' _w_sys_preset: '{{ (_preset_w_c * 9/5) if (is_imperial and _preset_w_c is not none) else _preset_w_c }}' _s_sys_preset: '{{ (_preset_s_c * 9/5) if (is_imperial and _preset_s_c is not none) else _preset_s_c }}' _sh_sys_preset: '{{ (_preset_sh_c * 9/5) if (is_imperial and _preset_sh_c is not none) else _preset_sh_c }}' winter_bias_sys: '{{ _w_sys_preset if (regional_defaults_enable and _w_sys_preset is not none) else winter_bias_sys_manual }}' summer_bias_sys: '{{ _s_sys_preset if (regional_defaults_enable and _s_sys_preset is not none) else summer_bias_sys_manual }}' shoulder_bias_sys: '{{ _sh_sys_preset if (regional_defaults_enable and _sh_sys_preset is not none) else shoulder_bias_sys_manual }}' _season_raw: "{% if season_entity_id %}\n {{ states(season_entity_id) | string | lower }}\n{% else %}\n {% set m = now().month %}\n {% if m in [12,1,2] %} winter\n {% elif m in [6,7,8] %} summer\n {% elif m in [3,4,5] %} spring\n {% else %} autumn {% endif %}\n{% endif %}\n" season_now: '{{ _season_raw }}' season_bias_sys_active: '{% if season_now == ''winter'' %} {{ winter_bias_sys | float(0) }} {% elif season_now == ''summer'' %} {{ summer_bias_sys | float(0) }} {% else %} {{ shoulder_bias_sys | float(0) }} {% endif %} ' season_bias_c: '{{ (season_bias_sys_active|float * 5/9) if is_imperial else (season_bias_sys_active|float) }}' co2_gap: "{% if co2_enable and co2_in_id and co2_out_id %}\n {{ (states(co2_in_id) | float(0)) - (states(co2_out_id) | float(0)) }}\n{% else %} 0 {% endif %}\n" co2_dir: "{# CO₂-driven seasonal bias: nudge comfort targets if CO₂ gap exceeded #} {% if co2_enable and (co2_gap | float(0)) >= (co2_delta_ppm | float(0)) %}\n \ {% if season_now == 'winter' %} 1\n {% elif season_now == 'summer' %} -1\n \ {% else %} 0 {% endif %}\n{% else %} 0 {% endif %}\n" co2_bias_c_eff: "{% set mag = co2_bias_max_c | float(0) %} {% if co2_dir != 0 %}\n \ {{ co2_dir * mag }}\n{% else %} 0 {% endif %}\n" dv_c: '{{ (dv_sys|float * 5/9) if is_imperial else (dv_sys|float) }}' t_adapt_base_c: '{{ (18.9 + 0.255 * t_out_c) | round(2) }}' out_valid: '{{ t_out_c >= 10 and t_out_c <= 40 }}' humid_offset_c: "{% if precision or humid_enable %}\n {% set rh = rh_in | float(50) %}\n {% if rh > 60 %}-0.3\n {% elif rh < 30 %}0.3\n {% else %}0.0{% endif %}\n{% else %}\n 0.0\n{% endif %}\n" velocity_offset_c: "{% set v = v_ms | float(0) %} {% if precision and v > 0.3 and t_in_c > 25 %}\n {% set scale = ((v - 0.3) / 0.7) %}\n {% set scale_clamped = [ [ scale, 1.0 ] | min, 0.0 ] | max %}\n {{ (0.5 * scale_clamped) | round(2) }}\n{% else %}\n 0.0\n{% endif %}\n" t_adapt_c_raw: "{% set base = t_adapt_base_c\n + season_bias_c\n + co2_bias_c_eff\n + sleep_bias_c_eff\n + learned_offset_c\n \ + humid_offset_c\n + velocity_offset_c %}\n{% if out_valid %}\n {{ base | round(2) }}\n{% else %}\n {{ (22.0\n + season_bias_c\n \ + co2_bias_c_eff\n + sleep_bias_c_eff\n + learned_offset_c\n \ + humid_offset_c\n + velocity_offset_c) | round(2) }}\n{% endif %}\n" t_adapt_c: '{{ (t_adapt_c_raw) | round(2) }}' t_adapt_c_guard: '{{ [ [ t_adapt_c, max_c ] | min , min_c ] | max | round(2) }}' band_min_c: '{{ [ [ t_adapt_c_guard - tol_c - setback_apply_c, min_c ] | max, max_c ] | min | round(2) }}' band_max_c: '{{ [ [ t_adapt_c_guard + tol_c + setback_apply_c, max_c ] | min, min_c ] | max | round(2) }}' band_min_c_final: '{{ [ [ [ band_min_c, band_max_c ] | min | round(2), max_c ] | min, min_c ] | max }}' band_max_c_final: '{{ [ [ [ band_min_c, band_max_c ] | max | round(2), max_c ] | min, min_c ] | max }}' inside_low_c: '{{ (t_adapt_c - inside_half_c_eff - (setback_apply_c/2)) | round(2) }}' inside_high_c: '{{ (t_adapt_c + inside_half_c_eff + (setback_apply_c/2)) | round(2) }}' temp_error_c: '{{ (t_in_c | float(0)) - (t_adapt_c_guard | float(0)) }}' temp_error_abs_c: '{{ temp_error_c | abs }}' t_adapt_sys: '{{ t_adapt_c * _to_sys_mult + _to_sys_add }}' band_min_sys: '{{ band_min_c_final * _to_sys_mult + _to_sys_add }}' band_max_sys: '{{ band_max_c_final * _to_sys_mult + _to_sys_add }}' inside_low_sys: '{{ inside_low_c * _to_sys_mult + _to_sys_add }}' inside_high_sys: '{{ inside_high_c * _to_sys_mult + _to_sys_add }}' min_allowed_raw: '{{ state_attr(climate_entity_id, ''min_temp'') | float(7.2) }}' max_allowed_raw: '{{ state_attr(climate_entity_id, ''max_temp'') | float(33.3) }}' climate_is_imperial: '{% set u = _u_climate | lower %} {% if ''f'' in u %}{{ true }} {% elif (max_allowed_raw | float(0)) >= 60 or (min_allowed_raw | float(0)) >= 30 %}{{ true }} {% else %}{{ false }}{% endif %} ' heat_deadband_cli: '{{ heat_deadband_sys | float(1.5) if climate_is_imperial else (heat_deadband_sys | float(1.5) * 5/9) }}' cool_deadband_cli: '{{ cool_deadband_sys | float(1.0) if climate_is_imperial else (cool_deadband_sys | float(1.0) * 5/9) }}' t_adapt_cli: '{{ (t_adapt_c_guard * 9/5 + 32) if climate_is_imperial else t_adapt_c_guard }}' band_min_cli: '{{ (band_min_c_final * 9/5 + 32) if climate_is_imperial else band_min_c_final }}' band_max_cli: '{{ (band_max_c_final * 9/5 + 32) if climate_is_imperial else band_max_c_final }}' inside_low_cli: '{{ (inside_low_c * 9/5 + 32) if climate_is_imperial else inside_low_c }}' inside_high_cli: '{{ (inside_high_c * 9/5 + 32) if climate_is_imperial else inside_high_c }}' min_allowed_cli: '{{ min_allowed_raw }}' max_allowed_cli: '{{ max_allowed_raw }}' safety_min_cli: '{{ (safety_min_c * 9/5 + 32) if climate_is_imperial else safety_min_c }}' safety_max_cli: '{{ (safety_max_c * 9/5 + 32) if climate_is_imperial else safety_max_c }}' safety_min_cli_capped: '{{ [[ safety_min_cli, max_allowed_cli ] | min, min_allowed_cli ] | max }}' safety_max_cli_capped: '{{ [[ safety_max_cli, max_allowed_cli ] | min, min_allowed_cli ] | max }}' hvac_modes: '{{ state_attr(climate_entity_id, ''hvac_modes'') | default([], true) }}' supports_auto: '{{ ''auto'' in hvac_modes }}' supports_heat_cool: '{{ ''heat_cool'' in hvac_modes }}' preferred_dual_mode: '{{ ''heat_cool'' if supports_heat_cool else (''auto'' if supports_auto else ''off'') }}' hvac_last_changed_ts: "{% set s = states[climate_entity_id] %} {% if s is not none %}\n {{ as_timestamp(s.last_changed) | float(0) }}\n{% else %}\n 0\n{% endif %}\n" mode_cooldown_sec: '{{ cooldown_min | int(0) * 60 }}' mode_switch_allowed: "{% if mode_cooldown_sec | int(0) <= 0 %}\n {{ true }}\n{% else %}\n {% set now_ts = as_timestamp(now()) | float(0) %}\n {% set delta = now_ts - hvac_last_changed_ts %}\n {{ delta >= (mode_cooldown_sec | float(0)) }}\n{% endif %}\n" fan_modes: '{{ state_attr(climate_entity_id, ''fan_modes'') | default([], true) }}' hvac_mode_now: '{{ states(climate_entity_id) | string | lower }}' is_in_dual_mode: '{{ hvac_mode_now in [''heat_cool'', ''auto''] }}' temp_now_sys: '{{ state_attr(climate_entity_id, ''temperature'') | float(0) }}' temp_low_now_sys: '{{ state_attr(climate_entity_id, ''target_temp_low'') | float(0) }}' temp_high_now_sys: '{{ state_attr(climate_entity_id, ''target_temp_high'') | float(0) }}' fan_now: '{{ state_attr(climate_entity_id, ''fan_mode'') | string | lower }}' fan_target_mode: "{% if not adaptive_fan or (fan_modes | length == 0) %}\n {{ fan_now }}\n{% else %}\n {% set e = temp_error_abs_c | float(0) %}\n {% set modes = fan_modes | list %}\n {% set has_high = 'high' in modes %}\n {% set has_med \ = 'medium' in modes %}\n {% set has_low = 'low' in modes %}\n {% set has_auto = 'auto' in modes %}\n\n {% if e < 0.3 %}\n {% if has_auto %}auto{% elif has_low %}low{% else %}{{ fan_now }}{% endif %}\n {% elif e < 1.0 %}\n {% if has_low %}low{% elif has_med %}medium{% else %}{{ fan_now }}{% endif %}\n {% else %}\n \ {% if has_high %}high{% elif has_med %}medium{% elif has_low %}low{% else %}{{ fan_now }}{% endif %}\n {% endif %}\n{% endif %}\n" cli_step: '{{ state_attr(climate_entity_id, ''target_temp_step'') | float(1.0 if climate_is_imperial else 0.5) }}' device_min_sep_cli: '{% set d1 = state_attr(climate_entity_id, ''min_temp_diff'') %} {% set d2 = state_attr(climate_entity_id, ''temperature_difference'') %} {% set v = d1 if d1 is not none else d2 %} {% set f = (v | float(0)) %} {{ f if f > 0 else 0 }} ' auto_sep_cli_from_user: "{% set sys = auto_sep_sys | float(0) %} {% if sys <= 0 %}0 {% else %}\n {{ (sys if climate_is_imperial else (sys * 5/9)) }}\n{% endif %}\n" thermostat_profile_val: !input thermostat_profile _ce_name: '{{ climate_entity_id | string | lower }}' _ce_manu: '{{ device_attr(climate_entity_id, ''manufacturer'') | default('''', true) | string | lower }}' _ce_model: '{{ device_attr(climate_entity_id, ''model'') | default('''', true) | string | lower }}' thermostat_profile_auto: "{% set n = _ce_name %} {% set m = _ce_manu %} {% set md = _ce_model %} {# Vendor-specific checks by manufacturer/model first, then name substrings #} {% if 'ecobee' in m or 'ecobee' in md or 'ecobee' in n %}\n Ecobee\n{% elif 'google' in m or 'nest labs' in m or 'nest' in md or 'nest' in n %}\n Google Nest\n{% elif ('resideo' in m) or ('honeywell' in m) %}\n {# Distinguish T-series vs broader Honeywell/Resideo lines #}\n {% if 't6 pro' in md and ('z-wave' in md or 'zwave' in md or 'z-wave' in n or 'zwave' in n) %}\n Honeywell T6 Pro Z-Wave\n {% elif 't5' in md or 't6' in md or 't9' in md or 't10' in md or 't series' in md %}\n Honeywell T-series (T5/T6/T9)\n {% else %}\n Honeywell Home/Resideo (Lyric/Prestige/VisionPRO)\n {% endif %}\n{% elif 'carrier' in m or 'carrier' in n %}\n Carrier Infinity/Edge\n{% elif 'bryant' in m or 'bryant' in n %}\n Bryant Evolution\n{% elif 'trane' in m or 'trane' in n or 'american standard' in m or 'american standard' in n %}\n Trane/American Standard\n{% elif 'lennox' in m or 'lennox' in n %}\n Lennox iComfort\n{% elif 'wyze' in m or 'wyze' in n %}\n Wyze\n{% elif 'bosch' in m or 'bosch' in n %}\n Bosch Connected Control\n{% elif 'amazon' in m or 'amazon' in n %}\n Amazon Smart Thermostat\n{% elif 'tado' in m or 'tado' in n %}\n Tado\n{% elif 'netatmo' in m or 'netatmo' in n %}\n \ Netatmo\n{% elif 'hive' in m or 'hive' in n %}\n Hive\n{% elif 'heatmiser' in m or 'heatmiser' in n %}\n Heatmiser Neo\n{% elif 'mysa' in m or 'mysa' in n %}\n Mysa (Electric Heat)\n{% elif 'tuya' in m or 'tuya' in n %}\n Tuya Zigbee (Generic)\n{% elif 'zigbee' in m or 'zigbee' in n or 'zha' in n %}\n Zigbee (ZHA Generic)\n{% elif 'z-wave' in m or 'zwave' in m or 'z-wave' in n or 'zwave' in n %}\n Z-Wave (ZWAVE_JS Generic)\n{% else %}\n Generic (no minimum)\n{% endif %}\n" thermostat_profile_eff: '{% set sel = thermostat_profile_val | string %} {% if sel == ''Auto (detect/device)'' %} {{ thermostat_profile_auto }} {% else %} {{ sel }} {% endif %} ' vendor_min_override_sys_in: !input vendor_min_override_sys _vendor_sep_lut_f: "{{ {\n 'Ecobee': 5.0,\n 'Google Nest': 3.0,\n 'Honeywell T-series (T5/T6/T9)': 3.0,\n 'Honeywell Home/Resideo (Lyric/Prestige/VisionPRO)': 2.0,\n 'Honeywell T6 Pro Z-Wave': 3.0,\n 'Emerson Sensi': 2.0,\n 'Carrier Infinity/Edge': 2.0,\n 'Bryant Evolution': 2.0,\n 'Trane/American Standard': 3.0,\n 'Lennox iComfort': 3.0,\n 'Bosch Connected Control': 2.0,\n 'Amazon Smart Thermostat': 2.0,\n 'Wyze': 5.0,\n 'Tado': 3.0,\n 'Netatmo': 3.0,\n 'Hive': 3.0,\n 'Heatmiser Neo': 1.8,\n 'Mysa (Electric Heat)': 3.0,\n 'Tuya Zigbee (Generic)': 1.8,\n \ 'Zigbee (ZHA Generic)': 1.8,\n 'Z-Wave (ZWAVE_JS Generic)': 1.8\n} }}\n" vendor_sep_cli_profile: '{% set p = thermostat_profile_eff | string %} {% set sep_f = _vendor_sep_lut_f.get(p, 0) | float(0) %} {{ sep_f if climate_is_imperial else (sep_f * 5/9) }} ' vendor_sep_cli_override: "{% set sys = vendor_min_override_sys_in | float(0) %} {% if sys <= 0 %}\n 0\n{% else %}\n {{ sys if climate_is_imperial else (sys * 5/9) }}\n{% endif %}\n" vendor_sep_cli: '{{ [vendor_sep_cli_override | float(0), vendor_sep_cli_profile | float(0)] | max }}' sep_cli_min: '{{ [ auto_sep_cli_from_user | float(0), device_min_sep_cli | float(0), vendor_sep_cli | float(0) ] | max }}' t_adapt_cli_capped: '{{ [[ t_adapt_cli, max_allowed_cli ] | min, min_allowed_cli ] | max }}' band_min_cli_capped: '{{ [[ band_min_cli, max_allowed_cli ] | min, min_allowed_cli ] | max }}' band_max_cli_capped: '{{ [[ band_max_cli, max_allowed_cli ] | min, min_allowed_cli ] | max }}' band_min_cli_q: '{{ ((band_min_cli_capped / cli_step) | round(0,''floor'') * cli_step) }}' band_max_cli_q: '{{ ((band_max_cli_capped / cli_step) | round(0,''floor'') * cli_step) }}' t_adapt_cli_q: '{{ ((t_adapt_cli_capped / cli_step) | round(0,''floor'') * cli_step) }}' sep_needed: '{{ sep_cli_min | float(0) if (supports_auto or supports_heat_cool) else 0 }}' delta_q: '{{ (band_max_cli_q - band_min_cli_q) | float(0) }}' need_expand: '{{ sep_needed > 0 and delta_q < sep_needed }}' low_cli_pref: '{{ band_min_cli_q }}' high_cli_pref: '{{ band_max_cli_q }}' half_sep: '{{ (sep_needed / 2.0) | float(0) }}' low_cli_exp: '{{ ((t_adapt_cli_q - half_sep) / cli_step) | round(0,''floor'') * cli_step }}' high_cli_exp: '{{ low_cli_exp + ( (sep_needed / cli_step) | round(0,''ceil'') * cli_step ) }}' target_low_cli_unc: '{{ low_cli_exp if need_expand else low_cli_pref }}' target_high_cli_unc: '{{ high_cli_exp if need_expand else high_cli_pref }}' target_low_cli_cap: '{{ [[ target_low_cli_unc, max_allowed_cli ] | min, min_allowed_cli ] | max }}' target_high_cli_cap: '{{ [[ target_high_cli_unc, max_allowed_cli ] | min, min_allowed_cli ] | max }}' target_low_cli: '{{ [ [ target_low_cli_cap, target_high_cli_cap ] | min , min_allowed_cli ] | max }}' target_high_cli: '{{ [ [ target_high_cli_cap, target_low_cli_cap ] | max , min_allowed_cli ] | max }}' _setpoint_tolerance: '{{ cli_step * 0.6 }}' _dual_mode_matches: "{% if not (supports_auto or supports_heat_cool) %}\n {{ false }}\n{% else %}\n {% set low_diff = (temp_low_now_sys - target_low_cli) | abs %}\n {% set high_diff = (temp_high_now_sys - target_high_cli) | abs %}\n {# Only update if change exceeds the relevant deadband (or rounding tolerance, whichever is smaller) #}\n {% set low_tol = [_setpoint_tolerance, heat_deadband_cli | float(1.5)] | min %}\n {% set high_tol = [_setpoint_tolerance, cool_deadband_cli | float(1.0)] | min %}\n {% set low_match = low_diff < low_tol %}\n {% set high_match = high_diff < high_tol %}\n {{ low_match and high_match }}\n{% endif %}\n" _single_mode_matches: "{% if hvac_mode_now not in ['cool','heat'] %}\n {{ false }}\n{% else %}\n {% set diff = (temp_now_sys - t_adapt_cli_q) | abs %}\n {% set tol = heat_deadband_cli if hvac_mode_now == 'heat' else cool_deadband_cli %}\n {% set effective_tol = [_setpoint_tolerance, tol | float(1.0)] | min %}\n \ {{ diff < effective_tol }}\n{% endif %}\n" needs_temp_update: '{{ not (_dual_mode_matches or _single_mode_matches) }}' nat_vent_would_activate: "{{ nat_vent_enable\n and (not vent_requires_occupancy or occ_now | bool)\n and (t_out_c is number) and (t_in_c is number)\n and ((t_out_c - t_in_c) | abs <= dv_c | float(1.0))\n and (\n (not psychro_enable)\n \ or (\n (dp_out_c is not none and dp_in_c is not none and (dp_out_c | float - dp_in_c | float) <= muggy_delta_dp_c | float(1.0))\n and ((h_out is not none and h_in is not none and (h_in | float - h_out | float) >= econ_dh | float(0)) or\n (ah_out is not none and ah_in is not none and (ah_in | float - ah_out | float) >= econ_dah | float(0)))\n and (dp_out_c is not none and (dp_out_c | float <= max_outdoor_dp_c | float(30)))\n \ )\n )\n}}\n" climate_unit_sym: '{{ ''°F'' if climate_is_imperial else ''°C'' }}' dbg_prefix: Adaptive Climate (final v{{ blueprint_version }}) trigger: - platform: state id: indoor_temp_state entity_id: !input indoor_temp_sensor for: seconds: 30 - platform: state id: outdoor_temp_state entity_id: !input outdoor_temp_sensor for: seconds: 60 - platform: state id: occupancy_state entity_id: !input occupancy_sensor - platform: state id: season_state entity_id: !input season_entity - platform: state id: pause_any entity_id: !input pause_sensors - platform: state id: climate_manual_change entity_id: !input climate_entity attribute: temperature - platform: state id: climate_manual_change_low entity_id: !input climate_entity attribute: target_temp_low - platform: state id: climate_manual_change_high entity_id: !input climate_entity attribute: target_temp_high - platform: state id: rh_in_state entity_id: !input indoor_humidity_sensor - platform: state id: rh_out_state entity_id: !input outdoor_humidity_sensor - platform: state id: rmot_state entity_id: !input rmot_sensor - platform: state id: baro_state entity_id: !input baro_pressure_sensor - platform: time_pattern id: tick_10m minutes: /10 - platform: time_pattern id: tick_hour hours: '*' minutes: 0 - platform: homeassistant id: ha_start event: start condition: [] action: - variables: _manual_trigger: '{{ trigger.id in [''climate_manual_change'', ''climate_manual_change_low'', ''climate_manual_change_high''] }} ' _manual_change_detected: "{% if not _manual_trigger %}\n {{ false }}\n{% else %}\n {# Convert tolerance delta properly (no offset subtraction for deltas) #}\n {% set tol_cli = manual_override_tolerance | float(1.0) %}\n\n {# Get current thermostat setpoints from trigger #}\n {% if trigger.id == 'climate_manual_change' %}\n {% set current_temp_cli = trigger.to_state.attributes.temperature | float(0) %}\n {% set is_dual_mode = false %}\n {% elif trigger.id in ['climate_manual_change_low','climate_manual_change_high'] %}\n {% set current_low_cli = trigger.to_state.attributes.target_temp_low | float(0) %}\n {% set current_high_cli = trigger.to_state.attributes.target_temp_high | float(0) %}\n {% set is_dual_mode = true %}\n {% else %}\n {% set current_temp_cli = 0 %}\n {% set is_dual_mode = false %}\n {% endif %}\n\n {# Guard: reject null/zero attributes from HVAC mode switches #}\n {# When mode changes (heat to heat_cool), attributes go null -> float(0) which poisons learning #}\n {% set min_sane = 5 if not climate_is_imperial else 40 %}\n {% if is_dual_mode and (current_low_cli < min_sane or current_high_cli < min_sane) %}\n {{ false }}\n {% elif not is_dual_mode and current_temp_cli < min_sane %}\n {{ false }}\n {% else %}\n {# Compare against what automation WOULD set (not what it predicted) #}\n {# This prevents detecting our own changes as manual overrides #}\n {% if is_dual_mode %}\n {# For dual setpoint: check if current matches our target_low/high within tolerance #}\n {% set low_diff = (current_low_cli - target_low_cli) | abs %}\n {% set high_diff = (current_high_cli - target_high_cli) | abs %}\n {% set is_automation_setpoint = (low_diff < tol_cli) and (high_diff < tol_cli) %}\n {% else %}\n {# For single setpoint: check if current matches our target within tolerance #}\n {% set temp_diff = (current_temp_cli - t_adapt_cli_q) | abs %}\n {% set is_automation_setpoint = temp_diff < tol_cli %}\n {% endif %}\n \n {# It's a manual override if the setpoint does NOT match what automation would set #}\n {{ not is_automation_setpoint }}\n {% endif %}\n{% endif %}\n" - variables: _t_in_valid: '{{ t_in_c is number and t_in_c > -40 and t_in_c < 60 }}' _t_out_valid: '{{ t_out_c is number and t_out_c > -60 and t_out_c < 60 }}' - choose: - conditions: - condition: template value_template: '{{ not _t_in_valid or not _t_out_valid }}' sequence: - choose: - conditions: - condition: template value_template: '{{ dbg in [''basic'',''verbose''] }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Skipped: sensor invalid | t_in={{ t_in_c }}°C | t_out={{ t_out_c }}°C' - stop: Sensor invalid - aborting run - choose: - conditions: - condition: template value_template: '{{ _manual_change_detected }}' sequence: - variables: _manual_temp_cli: "{% set min_sane = 5 if not climate_is_imperial else 40 %}\n{% set max_sane = 45 if not climate_is_imperial else 113 %}\n{% if trigger.id == 'climate_manual_change' %}\n {% set val = trigger.to_state.attributes.temperature | float(0) %}\n{% elif trigger.id == 'climate_manual_change_low' %}\n {# User adjusted heating setpoint - compare against our target_low #}\n {% set val = trigger.to_state.attributes.target_temp_low | float(0) %}\n{% elif trigger.id == 'climate_manual_change_high' %}\n {# User adjusted cooling setpoint - compare against our target_high #}\n {% set val = trigger.to_state.attributes.target_temp_high | float(0) %}\n{% else %}\n {% set val = 0 %}\n{% endif %}\n{# Sanity guard: reject values outside allowed thermostat range #}\n{{ val if (val >= min_sane and val <= max_sane) else 0 }}\n" _predicted_temp_cli: "{% if trigger.id == 'climate_manual_change' %}\n {{ t_adapt_cli_q }}\n{% elif trigger.id == 'climate_manual_change_low' %}\n \ {# Compare against what we would have set for heating #}\n {{ target_low_cli }}\n{% elif trigger.id == 'climate_manual_change_high' %}\n {# Compare against what we would have set for cooling #}\n {{ target_high_cli }}\n{% else %}{{ t_adapt_cli_q }}{% endif %}\n" _manual_temp_c: '{{ ((_manual_temp_cli | float) - 32) * 5/9 if climate_is_imperial else (_manual_temp_cli | float) }}' _predicted_temp_c: '{{ ((_predicted_temp_cli | float) - 32) * 5/9 if climate_is_imperial else (_predicted_temp_cli | float) }}' _error_c: '{{ (_manual_temp_c | float) - (_predicted_temp_c | float) }}' _is_heat_adjustment: "{% if trigger.id == 'climate_manual_change_low' %}\n \ {{ true }}\n{% elif trigger.id == 'climate_manual_change_high' %}\n {{ false }}\n{% else %}\n {# Single setpoint mode - check HVAC mode #}\n {{ hvac_mode_now == 'heat' }}\n{% endif %}\n" _offset_key: '{% set mode = ''heat'' if _is_heat_adjustment else ''cool'' %} {% set time = ''night'' if _is_night_now else ''day'' %} {% set season = _learning_season %} {{ mode ~ ''_'' ~ time ~ ''_'' ~ season }} ' _old_offset_sys: '{{ _learned_prefs_vars.get(_offset_key, 0) | float(0) }}' _old_offset_c: '{{ (_old_offset_sys * 5/9) if is_imperial else _old_offset_sys }}' _alpha: '{{ learning_rate | float(0.15) }}' _new_offset_c: '{% set alpha = learning_rate | float(0.15) %} {% set old_offset = _old_offset_c | float(0) %} {% set error = (_manual_temp_c | float(0)) - (_predicted_temp_c | float(0)) %} {% set raw = ((1 - alpha) * old_offset + alpha * error) | round(4) %} {% set max_offset = 5.0 %} {{ [[-max_offset, raw] | max, max_offset] | min }} ' _new_offset_sys: '{{ (_new_offset_c | float) * 9/5 if is_imperial else (_new_offset_c | float) }}' - choose: - conditions: - condition: template value_template: '{{ (learn_from_overrides | bool) and (learned_prefs_sensor_id | string | length > 0) and (_manual_temp_cli | float(0) > 0) }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Learning: key={{ _offset_key }} | old={{ _old_offset_sys | round(2) }}{{ unit_sym }} | new={{ _new_offset_sys | round(2) }}{{ unit_sym }} | error={{ _error_c | round(2) }}°C' - event: set_variable event_data: key: '{{ _offset_key }}' value: '{{ _new_offset_sys | round(2) }}' - choose: - conditions: - condition: template value_template: '{{ manual_override_enable and (manual_override_duration_min | int(0) > 0) and (manual_override_until_id | string | length > 0) }}' sequence: - variables: _override_until_ts: '{% set now_ts = as_timestamp(now()) %} {% set duration_sec = (manual_override_duration_min | int(60)) * 60 %} {{ (now_ts + duration_sec) | int }} ' - service: input_datetime.set_datetime target: entity_id: '{{ manual_override_until_id }}' data: timestamp: '{{ _override_until_ts | int }}' - choose: - conditions: - condition: template value_template: '{{ dbg in [''basic'',''verbose''] }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Debug: override helper set | helper={{ manual_override_until_id }} | timestamp={{ _override_until_ts }} | datetime={{ _override_until_ts | timestamp_custom(''%Y-%m-%d %H:%M:%S'') }}' - choose: - conditions: - condition: template value_template: '{{ dbg in [''basic'',''verbose''] }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Manual override: trigger={{ trigger.id }} | manual={{ _manual_temp_c | round(1) }}°C | predicted={{ _predicted_temp_c | round(1) }}°C | error={{ _error_c | round(2) }}°C{% if learn_from_overrides and learned_prefs_sensor_id %} | learned={{ _offset_key }}:{{ _old_offset_sys | round(2) }}|{{ _new_offset_sys | round(2) }}{{ unit_sym }}{% endif %} | pause={{ manual_override_duration_min }}min{% if not manual_override_until_id %} | WARNING: no helper{% endif %} ' - choose: - conditions: - condition: template value_template: '{{ manual_override_enable and (manual_override_duration_min | int(0) > 0) }}' sequence: !input manual_override_action - choose: - conditions: - condition: template value_template: '{{ manual_override_enable and (manual_override_duration_min | int(0) > 0) }}' sequence: - stop: Manual override active - stopping automation - choose: - conditions: - condition: template value_template: '{{ dbg == ''verbose'' and manual_override_enable and (manual_override_until_id | string | length > 0) }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Debug: override check | helper={{ manual_override_until_id }} | until_ts={{ state_attr(manual_override_until_id, ''timestamp'') }} | now_ts={{ as_timestamp(now()) | int }} | active={{ is_override_active }} ' - choose: - conditions: - condition: template value_template: '{{ is_override_active and trigger.id != ''pause_any'' }}' sequence: - choose: - conditions: - condition: template value_template: '{{ dbg in [''basic'',''verbose''] }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Skipped: manual override active | expires={{ states(manual_override_until_id) }}' - stop: Manual override active - skipping this run - choose: - conditions: - condition: template value_template: '{{ dbg in [''basic'',''verbose''] }}' sequence: - variables: _dbg_basic: '{{ dbg_prefix }} Band={{ band_min_sys | round(3) }}-{{ band_max_sys | round(3) }}{{ unit_sym }}; Set={{ t_adapt_sys | round(1) }}{{ unit_sym }}; In={{ t_in_sys | round(2) }}{{ unit_sym }}; Out={{ ((t_out_c * 9/5 + 32) if is_imperial else t_out_c) | round(2) }}{{ unit_sym }}; Trigger={{ (trigger.id if trigger is defined) if (trigger is defined) else ''unknown'' }} ; sep_enforce={{ sep_needed > 0 }} sep_min={{ sep_cli_min | round(1) }}{{ climate_unit_sym }}; step={{ cli_step }} bounds={{ safety_min_sys_in }}–{{ safety_max_sys_in }} ' - service: logbook.log data: name: Adaptive Comfort message: '{{ _dbg_basic }}' - conditions: - condition: template value_template: '{{ dbg == ''verbose'' }}' sequence: - variables: _dbg_verbose: '{{ dbg_prefix }} Band={{ band_min_sys | round(3) }}-{{ band_max_sys | round(3) }}{{ unit_sym }}; Set={{ t_adapt_sys | round(1) }}{{ unit_sym }}; In={{ t_in_sys | round(2) }}{{ unit_sym }}; Out={{ ((t_out_c * 9/5 + 32) if is_imperial else t_out_c) | round(2) }}{{ unit_sym }}; dp_out={{ dp_out_c | default(none) | round(1) if dp_out_c is not none else ''n/a'' }}C, dp_in={{ dp_in_c | default(none) | round(1) if dp_in_c is not none else ''n/a'' }}C, AH_out={{ ah_out | default(none) if ah_out is not none else ''n/a'' }} g/m³, AH_in={{ ah_in | default(none) if ah_in is not none else ''n/a'' }} g/m³, h_out={{ h_out | default(none) if h_out is not none else ''n/a'' }} kJ/kg, h_in={{ h_in | default(none) if h_in is not none else ''n/a'' }} kJ/kg; Trigger={{ (trigger.id if trigger is defined) if (trigger is defined) else ''unknown'' }} ; sep_enforce={{ sep_needed > 0 }} sep_min={{ sep_cli_min | round(1) }}{{ climate_unit_sym }}; step={{ cli_step }} bounds={{ safety_min_sys_in }}–{{ safety_max_sys_in }} ' - service: logbook.log data: name: Adaptive Comfort message: '{{ _dbg_verbose }}' - variables: any_open_now: "{% set ents = pause_entities if pause_entities is iterable else [] %} {% if ents|length > 0 %}\n {{ expand(ents) | selectattr('state','eq','on') | list | count > 0 }}\n{% else %} false {% endif %}\n" pause_blocked_now: "{{ (freeze_protect_enable and (t_in_sys|float(0) <= freeze_guard_sys_in|float(45)))\n \ or (overheat_protect_enable and (t_in_sys|float(0) >= overheat_guard_sys_in|float(88))) }}\n" - choose: - conditions: - condition: template value_template: '{{ any_open_now and not pause_blocked_now }}' sequence: - delay: seconds: '{{ pause_open_delay_eff }}' - service: climate.set_hvac_mode target: entity_id: !input climate_entity data: hvac_mode: 'off' - choose: - conditions: - condition: template value_template: '{{ true }}' sequence: !input pause_action - choose: - conditions: - condition: template value_template: '{{ pause_timeout_min_val|int(0) > 0 }}' sequence: - delay: minutes: '{{ pause_timeout_min_val|int(0) }}' - variables: _still_open: "{% set ents = pause_entities if pause_entities is iterable else [] %} {% if ents|length > 0 %}\n {{ expand(ents) | selectattr('state','eq','on') | list | count > 0 }}\n{% else %} false {% endif %}\n" - choose: - conditions: - condition: template value_template: '{{ _still_open }}' sequence: !input pause_timeout_action - choose: - conditions: - condition: template value_template: '{{ (not any_open_now) or pause_blocked_now }}' sequence: - delay: seconds: '{{ pause_close_delay_eff }}' - choose: - conditions: - condition: template value_template: '{{ true }}' sequence: !input resume_action - choose: - conditions: - condition: template value_template: '{{ hvac_mode_now == ''off'' and safety_reenable_freeze_needed }}' sequence: - service: climate.set_hvac_mode target: entity_id: !input climate_entity data: hvac_mode: heat - service: climate.set_temperature target: entity_id: !input climate_entity data: temperature: '{{ safety_min_cli_capped | float }}' - stop: Safety freeze-protect active - holding minimum temperature - conditions: - condition: template value_template: '{{ hvac_mode_now == ''off'' and safety_reenable_overheat_needed }}' sequence: - service: climate.set_hvac_mode target: entity_id: !input climate_entity data: hvac_mode: cool - service: climate.set_temperature target: entity_id: !input climate_entity data: temperature: '{{ safety_max_cli_capped | float }}' - stop: Safety overheat-protect active - holding maximum temperature - choose: - conditions: - condition: template value_template: '{{ is_override_active }}' sequence: - service: climate.set_hvac_mode target: entity_id: !input climate_entity data: hvac_mode: '{{ preferred_dual_mode }}' - choose: - conditions: - condition: template value_template: '{{ dbg in [''basic'',''verbose''] }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Resumed from pause during override | mode={{ preferred_dual_mode }}' - stop: Override active - HVAC restored after pause without changing setpoints - choose: - conditions: - condition: template value_template: "{{ nat_vent_enable\n and (not vent_requires_occupancy or occ_now | bool)\n and ( (t_out_c is number) and (t_in_c is number) )\n and ( (t_out_c - t_in_c)|abs <= dv_c|float(1.0) )\n and (\n (not psychro_enable)\n or (\n (dp_out_c is not none and dp_in_c is not none and (dp_out_c|float - dp_in_c|float) <= muggy_delta_dp_c|float(1.0))\n \ and ( (h_out is not none and h_in is not none and (h_in|float - h_out|float) >= econ_dh|float(0)) or\n (ah_out is not none and ah_in is not none and (ah_in|float - ah_out|float) >= econ_dah|float(0)) )\n and (dp_out_c is not none and (dp_out_c|float <= max_outdoor_dp_c|float(30)))\n \ )\n )\n}}\n" sequence: - service: climate.set_hvac_mode target: entity_id: !input climate_entity data: hvac_mode: 'off' - choose: - conditions: - condition: template value_template: '{{ dbg in [''basic'',''verbose''] }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Natural ventilation: HVAC off | conditions favorable' - stop: Natural ventilation active - skipping HVAC control - choose: - conditions: - condition: template value_template: '{{ supports_auto or supports_heat_cool }}' sequence: - choose: - conditions: - condition: template value_template: '{{ not is_in_dual_mode and mode_switch_allowed and preferred_dual_mode != ''off'' }}' sequence: - service: climate.set_hvac_mode target: entity_id: !input climate_entity data: hvac_mode: '{{ preferred_dual_mode }}' - choose: - conditions: - condition: template value_template: '{{ dbg == ''verbose'' }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Mode change: old={{ hvac_mode_now }} | new={{ preferred_dual_mode }}' - conditions: - condition: template value_template: '{{ not is_in_dual_mode and not mode_switch_allowed and dbg in [''basic'',''verbose''] }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Mode change blocked: cooldown | target={{ preferred_dual_mode }} | wait={{ cooldown_min }}min' - choose: - conditions: - condition: template value_template: '{{ needs_temp_update }}' sequence: - service: climate.set_temperature target: entity_id: !input climate_entity data: target_temp_low: '{{ target_low_cli | float }}' target_temp_high: '{{ target_high_cli | float }}' - choose: - conditions: - condition: template value_template: '{{ dbg == ''verbose'' }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Temps updated: old={{ temp_low_now_sys | round(1) }}/{{ temp_high_now_sys | round(1) }}{{ climate_unit_sym }} | new={{ target_low_cli | round(1) }}/{{ target_high_cli | round(1) }}{{ climate_unit_sym }}' default: - choose: - conditions: - condition: template value_template: '{{ dbg == ''verbose'' }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Skipped: setpoints unchanged | current={{ target_low_cli | round(1) }}/{{ target_high_cli | round(1) }}{{ climate_unit_sym }}' - conditions: - condition: template value_template: '{{ hvac_mode_now in [''cool'',''heat''] }}' sequence: - choose: - conditions: - condition: template value_template: '{{ needs_temp_update }}' sequence: - service: climate.set_temperature target: entity_id: !input climate_entity data: temperature: '{{ t_adapt_cli_q | float }}' - choose: - conditions: - condition: template value_template: '{{ dbg == ''verbose'' }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Temp updated: old={{ temp_now_sys | round(1) }}{{ climate_unit_sym }} | new={{ t_adapt_cli_q | round(1) }}{{ climate_unit_sym }}' default: - choose: - conditions: - condition: template value_template: '{{ dbg == ''verbose'' }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Skipped: setpoint unchanged | current={{ t_adapt_cli_q | round(1) }}{{ climate_unit_sym }}' - choose: - conditions: - condition: template value_template: "{{ adaptive_fan\n and (fan_modes | length > 0)\n and (fan_target_mode | string | lower) != (fan_now | string | lower) }}\n" sequence: - service: climate.set_fan_mode target: entity_id: !input climate_entity data: fan_mode: '{{ fan_target_mode }}' - choose: - conditions: - condition: template value_template: '{{ dbg == ''verbose'' }}' sequence: - service: logbook.log data: name: Adaptive Comfort message: 'Fan mode updated: old={{ fan_now }} | new={{ fan_target_mode }}' - choose: - conditions: - condition: template value_template: '{{ (hvac_mode_now != ''off'' or safety_reenable_needed) and not nat_vent_would_activate }}' sequence: - service: climate.turn_on target: entity_id: !input climate_entity mode: restart