###################################################################### # @CCOSTAN - Follow Me on X # For more info visit https://www.vcloudinfo.com/click-here # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Climate Control - Seasonal HVAC automations and sensors. # Thermostat helpers for upstairs/downstairs comfort. # ------------------------------------------------------------------- # Related Issue: 1571 # Notes: Filter due alerts use a 350h runtime threshold, 9-month hard limit, 3-day snooze, and Filter Changed push actions. # Notes: Condenser line cleanout uses a single 360-day reminder for both HVAC systems. # Video: https://youtu.be/y47KSflS1aw # Blog: https://www.vcloudinfo.com/2026/06/home-assistant-notification-snooze-buttons.html ###################################################################### # Contact: @CCOSTAN | Entity reference details below # Entity reference and attribute summaries: # # Entity ID: climate.downstairs # Supported hvac_modes: heat, cool, heat_cool, off # Supported fan_modes: on, off # Supported preset_modes: none, eco # Supported temperature range: 50°F (min_temp) – 90°F (max_temp) # # Current state (example): # current_temperature: 77 # temperature: 83 # target_temp_high: null # target_temp_low: null # current_humidity: 58 # fan_mode: off # hvac_action: idle # preset_mode: none # Supported features: 411 # -------------------------------------------------------------- # Entity ID: climate.upstairs # Supported hvac_modes: off, heat, cool, heat_cool # Supported temperature range: 50°F (min_temp) – 90°F (max_temp) # # Current state (example): # current_temperature: 76 # temperature: 78 # target_temp_high: null # target_temp_low: null # friendly_name: Upstairs # Supported features: 387 ###################################################################### # --------------------------------------------------------------------------- # Template helpers – compressor status flags and numeric runtime helpers # --------------------------------------------------------------------------- template: - binary_sensor: - name: "Downstairs AC is Cooling" unique_id: downstairs_ac_cooling state: > {% set action = state_attr('climate.downstairs', 'hvac_action') %} {% set current = state_attr('climate.downstairs', 'current_temperature') | float(none) %} {% set target = state_attr('climate.downstairs', 'temperature') | float(none) %} {{ action == 'cooling' or ( action is none and is_state('climate.downstairs', 'cool') and current is not none and target is not none and current > target ) }} - name: "Upstairs AC is Cooling" unique_id: upstairs_ac_cooling state: > {% set action = state_attr('climate.upstairs', 'hvac_action') %} {% set current = state_attr('climate.upstairs', 'current_temperature') | float(none) %} {% set target = state_attr('climate.upstairs', 'temperature') | float(none) %} {{ action == 'cooling' or ( action is none and is_state('climate.upstairs', 'cool') and current is not none and target is not none and current > target ) }} sensor: - name: "Downstairs AC Cooling Numeric" unique_id: downstairs_ac_cooling_numeric state: "{{ 1 if is_state('binary_sensor.downstairs_ac_is_cooling', 'on') else 0 }}" - name: "Upstairs AC Cooling Numeric" unique_id: upstairs_ac_cooling_numeric state: "{{ 1 if is_state('binary_sensor.upstairs_ac_is_cooling', 'on') else 0 }}" input_datetime: downstairs_last_filter_change: name: Downstairs Last Filter Change has_date: true has_time: true downstairs_filter_snooze_until: name: Downstairs Filter Snooze Until has_date: true has_time: true upstairs_last_filter_change: name: Upstairs Last Filter Change has_date: true has_time: true upstairs_filter_snooze_until: name: Upstairs Filter Snooze Until has_date: true has_time: true hvac_condenser_lines_last_cleaned: name: HVAC Condenser Lines Last Cleaned has_date: true has_time: false initial: "2026-06-28" hvac_condenser_lines_cleaning_snooze_until: name: HVAC Condenser Lines Cleaning Snooze Until has_date: true has_time: true initial: "2026-06-28 00:00:00" # --------------------------------------------------------------------------- # Integration sensors tally runtime based on compressor state # --------------------------------------------------------------------------- sensor: - platform: integration name: Downstairs AC Runtime Raw source: sensor.downstairs_ac_cooling_numeric unit_time: h method: left round: 2 - platform: integration name: Upstairs AC Runtime Raw source: sensor.upstairs_ac_cooling_numeric unit_time: h method: left round: 2 # --------------------------------------------------------------------------- # Utility‑meters that act like odometers (reset when you swap the filter) # --------------------------------------------------------------------------- utility_meter: downstairs_ac_runtime_since_last_filter_change: source: sensor.downstairs_ac_runtime_raw upstairs_ac_runtime_since_last_filter_change: source: sensor.upstairs_ac_runtime_raw script: reset_downstairs_filter: alias: Reset Downstairs Filter mode: queued sequence: - service: input_datetime.set_datetime data: entity_id: input_datetime.downstairs_last_filter_change datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" - service: input_datetime.set_datetime data: entity_id: input_datetime.downstairs_filter_snooze_until datetime: "{{ (now() - timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') }}" - service: utility_meter.reset target: entity_id: sensor.downstairs_ac_runtime_since_last_filter_change reset_upstairs_filter: alias: Reset Upstairs Filter mode: queued sequence: - service: input_datetime.set_datetime data: entity_id: input_datetime.upstairs_last_filter_change datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" - service: input_datetime.set_datetime data: entity_id: input_datetime.upstairs_filter_snooze_until datetime: "{{ (now() - timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') }}" - service: utility_meter.reset target: entity_id: sensor.upstairs_ac_runtime_since_last_filter_change reset_hvac_condenser_lines_cleaned: alias: Reset HVAC Condenser Lines Cleaned mode: queued sequence: - service: input_datetime.set_datetime data: entity_id: input_datetime.hvac_condenser_lines_last_cleaned date: "{{ now().strftime('%Y-%m-%d') }}" - service: input_datetime.set_datetime data: entity_id: input_datetime.hvac_condenser_lines_cleaning_snooze_until datetime: "{{ (now() - timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') }}" set_downstairs_target_temp_based_on_conditions: alias: Set Downstairs Target Temperature Based on Conditions mode: single sequence: - choose: - conditions: - condition: state entity_id: group.family state: 'not_home' sequence: - service: script.send_to_logbook data: topic: "CLIMATE" message: "Downstairs HVAC set to ECO (family not_home)." - service: climate.set_preset_mode data: entity_id: climate.downstairs preset_mode: 'eco' - conditions: - condition: and conditions: - condition: state entity_id: group.family state: 'home' - condition: template value_template: >- {{ states('sensor.pirateweather_temperature') in ['unknown','unavailable','none',''] }} sequence: - service: script.send_to_logbook data: topic: "CLIMATE" message: "Outside temp unavailable; allowing downstairs cool mode (defaulting to 80F)." - service: climate.set_hvac_mode data: entity_id: climate.downstairs hvac_mode: cool - service: climate.set_temperature data: entity_id: climate.downstairs temperature: 80 - conditions: - condition: and conditions: - condition: state entity_id: group.family state: 'home' - condition: numeric_state entity_id: sensor.pirateweather_temperature below: 78 sequence: - service: script.send_to_logbook data: topic: "CLIMATE" message: "Downstairs target set to 78F (family home, outside temp <78F)." - service: climate.set_hvac_mode data: entity_id: climate.downstairs hvac_mode: cool - service: climate.set_temperature data: entity_id: climate.downstairs temperature: 78 - conditions: - condition: and conditions: - condition: state entity_id: group.family state: 'home' - condition: numeric_state entity_id: sensor.pirateweather_temperature above: 92 sequence: - service: script.send_to_logbook data: topic: "CLIMATE" message: "Downstairs target set to 78F (family home, temp >92F)." - service: climate.set_hvac_mode data: entity_id: climate.downstairs hvac_mode: cool - service: climate.set_temperature data: entity_id: climate.downstairs temperature: 78 - conditions: - condition: and conditions: - condition: state entity_id: group.family state: 'home' - condition: template value_template: "{{ states('sensor.pirateweather_temperature')|float(-999) >= 78 }}" sequence: - service: script.send_to_logbook data: topic: "CLIMATE" message: "Downstairs target set to 80F (family home)." - service: climate.set_hvac_mode data: entity_id: climate.downstairs hvac_mode: cool - service: climate.set_temperature data: entity_id: climate.downstairs temperature: 80 set_downstairs_daytime_target: alias: Set Downstairs Daytime Target mode: single sequence: - service: script.set_downstairs_target_temp_based_on_conditions set_downstairs_hvac_cool: alias: Set Downstairs HVAC Cool mode: single sequence: - service: climate.set_hvac_mode data: entity_id: climate.downstairs hvac_mode: cool set_upstairs_cool_82: alias: Set Upstairs Cool 82 mode: single sequence: - service: climate.set_hvac_mode data: entity_id: climate.upstairs hvac_mode: cool - service: climate.set_temperature data: entity_id: climate.upstairs temperature: 82 ############################################################################## ### AUTOMATIONS - Thermostat schedules, guardrails, and presence/weather logic ### Some shutoff automations are also in the ALARM.yaml package when windows/doors are left open. ### There are also some automations in the POWERWALL.yaml package when the grid is down. ############################################################################## automation: - alias: Notify Climate Filter Change Due description: Notify when HVAC filter runtime exceeds 350h or age reaches 9 months trigger: - platform: numeric_state entity_id: sensor.downstairs_ac_runtime_since_last_filter_change above: 350 id: downstairs_runtime - platform: time at: "09:00:00" id: downstairs_daily - platform: numeric_state entity_id: sensor.upstairs_ac_runtime_since_last_filter_change above: 350 id: upstairs_runtime - platform: time at: "09:10:00" id: upstairs_daily variables: location: "{{ 'upstairs' if trigger.id.startswith('upstairs') else 'downstairs' }}" location_title: "{{ 'Upstairs' if location == 'upstairs' else 'Downstairs' }}" runtime_sensor: "sensor.{{ location }}_ac_runtime_since_last_filter_change" last_filter_entity: "input_datetime.{{ location }}_last_filter_change" snooze_entity: "input_datetime.{{ location }}_filter_snooze_until" runtime_hours: "{{ states(runtime_sensor) | float(0) }}" last_filter_ts: "{{ as_timestamp(states(last_filter_entity), 0) }}" filter_age_days: "{{ ((as_timestamp(now()) - (last_filter_ts | float(0))) / 86400) | round(0) }}" due_reason: >- {% set runtime_due = runtime_hours | float(0) > 350 %} {% set age_due = (last_filter_ts | float(0)) == 0 or (filter_age_days | int(0)) >= 274 %} {% if runtime_due and age_due %} Runtime has exceeded 350h and the filter is {{ filter_age_days }} days old. {% elif runtime_due %} Runtime has exceeded 350h. {% else %} Filter age has reached the 9-month hard limit ({{ filter_age_days }} days). {% endif %} condition: - condition: template value_template: >- {{ (runtime_hours | float(0) > 350) or (last_filter_ts | float(0)) == 0 or (filter_age_days | int(0)) >= 274 }} - condition: template value_template: >- {% set snooze_until = as_timestamp(states(snooze_entity), 0) %} {{ snooze_until <= as_timestamp(now()) }} action: - service: script.send_to_logbook data: topic: "MAINTENANCE" message: >- {{ location_title }} AC filter due. {{ due_reason }} Runtime is {{ runtime_hours | round(1) }}h. - service: script.notify_engine_two_button data: title: "Home Maintenance Reminder" value1: "It's time to change your {{ location_title }} AC filter." value2: > {{ due_reason }} Runtime is {{ runtime_hours | round(1) }}h. title1: "Snooze 3d" action1: "{{ 'SNOOZE_UPSTAIRS_FILTER_3D' if location == 'upstairs' else 'SNOOZE_DOWNSTAIRS_FILTER_3D' }}" icon1: "sfsymbols:clock" title2: "Filter Changed" action2: "{{ 'RESET_UPSTAIRS_FILTER' if location == 'upstairs' else 'RESET_DOWNSTAIRS_FILTER' }}" icon2: "sfsymbols:checkmark.circle" who: "carlo" group: "maintenance" level: "active" - alias: Climate Filter Reminder Actions id: 6d7056d0-90ce-4c4f-b8b1-fd32a7e58311 mode: queued trigger: - platform: event event_type: mobile_app_notification_action event_data: action: SNOOZE_DOWNSTAIRS_FILTER_3D id: downstairs_snooze - platform: event event_type: mobile_app_notification_action event_data: action: SNOOZE_UPSTAIRS_FILTER_3D id: upstairs_snooze - platform: event event_type: mobile_app_notification_action event_data: action: RESET_DOWNSTAIRS_FILTER id: downstairs_reset - platform: event event_type: mobile_app_notification_action event_data: action: RESET_UPSTAIRS_FILTER id: upstairs_reset variables: location: "{{ 'upstairs' if trigger.id.startswith('upstairs') else 'downstairs' }}" location_title: "{{ 'Upstairs' if location == 'upstairs' else 'Downstairs' }}" snooze_entity: "input_datetime.{{ location }}_filter_snooze_until" reset_script: "script.reset_{{ location }}_filter" action: - choose: - conditions: "{{ trigger.id.endswith('_snooze') }}" sequence: - variables: snooze_until: "{{ (now() + timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S') }}" - service: input_datetime.set_datetime target: entity_id: "{{ snooze_entity }}" data: datetime: "{{ snooze_until }}" - service: script.send_to_logbook data: topic: "MAINTENANCE" message: "{{ location_title }} AC filter reminder snoozed until {{ snooze_until }}." - conditions: "{{ trigger.id.endswith('_reset') }}" sequence: - service: "{{ reset_script }}" - service: script.send_to_logbook data: topic: "MAINTENANCE" message: "{{ location_title }} AC filter reset from notification action." - alias: Notify HVAC Condenser Lines Cleaning Due id: 4dafac96-3163-4d63-847a-76727005f75b description: Notify every 360 days when the outdoor condenser lines should be cleaned trigger: - platform: time at: "09:20:00" variables: cleaned: "{{ states('input_datetime.hvac_condenser_lines_last_cleaned') }}" cleaned_ts: >- {% if cleaned in ['unknown', 'unavailable', 'none', ''] %} 0 {% else %} {{ as_timestamp(strptime(cleaned, '%Y-%m-%d'), 0) }} {% endif %} days_since_cleaned: "{{ ((as_timestamp(now()) - (cleaned_ts | float(0))) / 86400) | round(0) }}" condition: - condition: template value_template: "{{ (cleaned_ts | float(0)) == 0 or (days_since_cleaned | int(0)) >= 360 }}" - condition: template value_template: >- {% set snooze_until = as_timestamp(states('input_datetime.hvac_condenser_lines_cleaning_snooze_until'), 0) %} {{ snooze_until <= as_timestamp(now()) }} action: - service: script.send_to_logbook data: topic: "MAINTENANCE" message: "HVAC condenser lines cleanout due; last cleaned {{ days_since_cleaned }} days ago." - service: script.notify_engine_two_button data: title: "Home Maintenance Reminder" value1: "Clean the HVAC condenser lines." value2: "360-day cleanout is due; last cleaned {{ days_since_cleaned }} days ago." title1: "Snooze 3d" action1: "SNOOZE_HVAC_CONDENSER_LINES_3D" icon1: "sfsymbols:clock" title2: "Cleaned" action2: "RESET_HVAC_CONDENSER_LINES_CLEANED" icon2: "sfsymbols:checkmark.circle" who: "carlo" group: "maintenance" level: "active" - alias: Climate Condenser Lines Reminder Actions id: dfe89869-4bd3-47f6-bf0d-612bf7ee949f mode: queued trigger: - platform: event event_type: mobile_app_notification_action event_data: action: SNOOZE_HVAC_CONDENSER_LINES_3D id: snooze - platform: event event_type: mobile_app_notification_action event_data: action: RESET_HVAC_CONDENSER_LINES_CLEANED id: reset action: - choose: - conditions: "{{ trigger.id == 'snooze' }}" sequence: - variables: snooze_until: "{{ (now() + timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S') }}" - service: input_datetime.set_datetime target: entity_id: input_datetime.hvac_condenser_lines_cleaning_snooze_until data: datetime: "{{ snooze_until }}" - service: script.send_to_logbook data: topic: "MAINTENANCE" message: "HVAC condenser lines cleanout reminder snoozed until {{ snooze_until }}." - conditions: "{{ trigger.id == 'reset' }}" sequence: - service: script.reset_hvac_condenser_lines_cleaned - service: script.send_to_logbook data: topic: "MAINTENANCE" message: "HVAC condenser lines cleanout reset from notification action." - alias: 'AC Status Announcement' id: 7812fdaf-a3f8-498b-8f07-28e977e528fe trigger: - platform: state entity_id: - climate.downstairs - climate.upstairs from: 'off' - platform: state entity_id: - climate.downstairs - climate.upstairs to: 'off' action: - service: script.speech_engine data: NestStatus: "The {{ trigger.entity_id.split('.')[1].split('_')[0]}} {{ trigger.entity_id.split('.')[1].split('_')[1]}} has now been turned on for {{(trigger.to_state.state)}}." call_window_check: 1 - alias: Prevent Upstairs Nest Below 76 id: 7812fdaf-a3f8-498b-8f07-28e977e528ff trigger: - platform: numeric_state entity_id: climate.upstairs attribute: temperature below: 76 condition: - condition: template value_template: "{{ is_state('binary_sensor.powerwall_grid_status', 'on') }}" action: - delay: "00:03:00" - service: climate.set_temperature data: entity_id: climate.upstairs temperature: 78 - service: script.send_to_logbook data: topic: "CLIMATE" message: "Upstairs target temp corrected to 78F (guardrail)." - alias: Set Downstairs Bedtime Target When Both in Bed id: set_downstairs_nest_77_both_in_bed trigger: - platform: state entity_id: group.bed to: 'on' condition: - condition: and conditions: - condition: state entity_id: binary_sensor.sleepnumber_carlo_carlo_is_in_bed state: 'on' - condition: state entity_id: binary_sensor.sleepnumber_carlo_stacey_is_in_bed state: 'on' - condition: state entity_id: binary_sensor.powerwall_grid_status state: 'on' - condition: state entity_id: sun.sun state: 'below_horizon' action: - choose: - conditions: - condition: numeric_state entity_id: sensor.pirateweather_temperature above: 80 sequence: - service: climate.set_temperature data: entity_id: climate.downstairs temperature: 77 - conditions: - condition: numeric_state entity_id: sensor.pirateweather_temperature above: 75 below: 80 sequence: - service: climate.set_temperature data: entity_id: climate.downstairs temperature: 79 - alias: 'Set Downstairs Thermostat Daytime Target' id: set_downstairs_thermostat_daytime_target mode: single trigger: - platform: state entity_id: sun.sun to: 'above_horizon' - platform: time at: "08:00:00" - platform: numeric_state entity_id: sensor.pirateweather_temperature above: 92 condition: - condition: and conditions: - condition: state entity_id: sun.sun state: 'above_horizon' - condition: state entity_id: input_boolean.guest_mode state: 'off' - condition: state entity_id: binary_sensor.powerwall_grid_status state: 'on' action: - service: script.set_downstairs_daytime_target # Set thermostats to eco mode when everyone is away - alias: 'Set Thermostats to Eco When Away' id: 1e2d3c4b-eco-thermostat-away mode: single trigger: - platform: state entity_id: group.family to: 'not_home' condition: - condition: state entity_id: group.family state: 'not_home' - condition: state entity_id: input_boolean.guest_mode state: 'off' action: - service: climate.set_preset_mode data: entity_id: climate.downstairs preset_mode: 'eco' - choose: - conditions: - condition: numeric_state entity_id: sensor.pirateweather_temperature below: 75 sequence: - service: script.send_to_logbook data: topic: "CLIMATE" message: "Skipping downstairs cool mode (outside temp <75F)." default: - service: script.set_downstairs_hvac_cool - service: climate.set_temperature data: entity_id: climate.upstairs temperature: 83 # Automation: Set Thermostats Back to Cool Mode - alias: 'Set Thermostats to Cool When Home' id: 1e2d3c4b-cool-thermostat-home mode: single trigger: - platform: state entity_id: group.family to: 'home' condition: - condition: or conditions: - condition: state entity_id: group.family state: 'home' - condition: template value_template: "{{ is_state('input_boolean.guest_mode', 'off') }}" - condition: state entity_id: binary_sensor.powerwall_grid_status state: 'on' action: - service: climate.set_preset_mode data: entity_id: climate.downstairs preset_mode: 'none' - service: climate.set_hvac_mode data: entity_id: climate.upstairs hvac_mode: cool - service: script.set_downstairs_target_temp_based_on_conditions - service: climate.set_temperature data: entity_id: climate.upstairs temperature: 82 - alias: 'Reset Upstairs to 82 Each Night' id: reset_upstairs_thermostat_nightly_82 mode: single trigger: - platform: time at: "03:00:00" condition: - condition: template value_template: "{{ states('binary_sensor.powerwall_grid_status') == 'on' }}" action: - service: script.set_upstairs_cool_82 - alias: 'Humidity Control' id: AC_Humidity_Control mode: single trigger: - platform: numeric_state entity_id: sensor.downstairs_humidity above: 60 - platform: time at: "02:00:00" condition: - condition: time after: "01:00:00" before: "08:00:00" - condition: numeric_state entity_id: sensor.downstairs_humidity above: 60 - condition: template #Only needed in the winter months value_template: "{{ now().month in [10, 11, 12, 1, 2, 3] }}" - condition: template # Only run if AC is idle (prevents fighting other automations) value_template: "{{ state_attr('climate.downstairs', 'hvac_action') == 'idle' }}" - condition: template # Never run if the grid is down and running on powerwall. value_template: "{{ states('binary_sensor.powerwall_grid_status')|lower == 'on' }}" action: - choose: - conditions: - condition: numeric_state entity_id: sensor.pirateweather_temperature below: 75 sequence: - service: script.send_to_logbook data: topic: "CLIMATE" message: "Skipping downstairs cool mode (outside temp <75F)." default: - service: climate.set_preset_mode data: entity_id: climate.downstairs preset_mode: 'none' - service: climate.set_hvac_mode data: entity_id: climate.downstairs hvac_mode: cool - service: climate.set_temperature data: entity_id: climate.downstairs temperature: 77 - delay: "00:20:00" - service: script.set_downstairs_target_temp_based_on_conditions - delay: "02:00:00" # stops it from being triggered again too soon.