Skip to content

Instantly share code, notes, and snippets.

@tetele
Last active March 1, 2024 21:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tetele/0e9b8720fac4bc08edd894e12d7976a2 to your computer and use it in GitHub Desktop.
Save tetele/0e9b8720fac4bc08edd894e12d7976a2 to your computer and use it in GitHub Desktop.
Home Assistant HVAC adjustment based on doors/windows

Home Assistant HVAC adjustment based on doors/windows

This blueprint helps you save money with heating/cooling by resetting the temperature for a certain thermostat if there is an open window or door. It is still a work in progress, but it works pretty OK with heating.

Each instance works with a single thermostat/opening pair, but you can define a group of windows (binary_sensors) if that's the case. Whenever a window/door is left open for more than the time specified, the thermostat will run a script that should turn off/turn down the HVAC

Installation

  1. copy blueprint_hvac_windows.yaml file in your /config/blueprints/automation folder or simply click Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled. DON'T CREATE ANY AUTOMATION JUST YET
  2. if you have an entry in configuration.yaml referencing homeassistant.pagkages, add the hvac_scripts.yaml file along with your other packages so that it can be interpreted as a package
  3. if you don't have such an entry, create a folder structure called packages/hvac_windows in your /config folder, copy hvac_scripts.yaml inside of it and replace the following lines in configuration.yaml (probably at the top of the file)
homeassistant: # <--- this line should have already been there
  packages: !include_dir_named packages # <--- this is the line you need to add
  1. edit the packages/hvac_windows/hvac_scripts.yaml file to customize what should be happening - look for ##### THIS IS THE PART THAT YOU SHOULD MODIFY ######
    • you can edit how you turn the thermostat down and how and who you notify when that happens
    • by default in the scripts, the thermostat goes to Eco mode (considering it has one) and it notifies whoever is home. If nobody is home, it notifies everyone (some notify services need to be set up for this to work)
    • you can also customize a part that, by default, clears notifications on phones/tables when things go back to normal
  2. restart Home Assistant to reload the changes
  3. if you don't have an external temperature sensor, create a template sensor from the weather integration to get the outside temperature
  4. create an input_boolean helper called something like " HVAC manual override", e.g. "Bedroom HVAC manual override"
  5. choose a room with a dedicated thermostat and a window/door sensor and create a new automation based on the blueprint (it should be called "Adjust HVAC temperature based on doors/windows")
    • choose the appropriate scripts from those imported - they are easy to find as their names are prefixed with HAVCW
    • Script for turning down HVAC - HVACW - Turn down HVAC
    • Script for turning HVAC back up - HVACW - Turn HVAC back up
    • Script for turning HVAC back up (override) - HVACW - Turn HVAC back up user override - this is only necessary if you have turned on the Allow user override toggle
    • Script for sending actionable notifications - HVACW - Notify that the HVAC was modified
    • Script for clearing notifications - HVACW - Clear HVAC notifications - this is only necessary if you have turned on the Allow user override toggle
  6. repeat for any rooms where you want the same behavior

Check out this short demo on YouTube

HA HVAC adjustment automation demo

Testing

Deploy the automation and open the physical window you have defined. After the set time, you will see that the turn_down script will run. Close the window. After some time, the thermostat is reset to the state it had prior to the turn_down script.

Limitations

There is a limitation due to a HA bug: having homeassistant -> shutdown as an event will not run all actions. Particularly, although notification services work, you can't alter entities.

As a consequence, if the temperature was lowered due to an open window when HA was restarted, because the previous state was saved to a scene that disappears upon restart and because we can't activate that scene just before the restart, there's no way to know if after the restart the temperature is set the way it is because of an automation or because you wanted it that way.

Related issues: home-assistant/core#76765 home-assistant/core#79919

GithubSponsor or BuyMeCoffee

blueprint:
name: Adjust HVAC temperature based on doors/windows
description: Send notifications or take action based on whether a door/window is needlessly open in the area you're trying to heat/cool
domain: automation
input:
hvac:
name: HVAC thermostat
description: The climate component which controls the HVAC in the room/area
selector:
entity:
domain: climate
external_temperature:
name: External temperature
description: A sensor referencing the temperature outside the room/area, in the area which the door/window opens to. This could be the next room temperature for doors or the outside temperature for windows.
selector:
entity:
domain:
- sensor
- input_number
window_sensor:
name: Window/door sensor
description: The window or door sensor to be monitored
selector:
entity:
domain:
- binary_sensor
- input_boolean
on_debounce_time:
name: Acceptable open duration
description: The amount of time a door/window is allowed to be open before any action is taken
default:
hours: 0
minutes: 5
seconds: 0
selector:
duration:
off_debounce_time:
name: Closed lag duration
description: The amount of time for which a door/window needes to be closed before any action is taken
default:
hours: 0
minutes: 0
seconds: 30
selector:
duration:
allow_user_override:
name: Allow user override
description: If this is set and a user or another automation alters the set temperature of the already-turned-down HVAC, then closing the window no longer resets its state to that from before the turning down took place
default: false
selector:
boolean:
override_boolean:
name: Override indicator (optional)
description: An input_boolean where to store whether the HVAC temperature was overridden while the door/window was open. Useful only if user overrides are allowed
selector:
entity:
domain: input_boolean
default: "a.b" # something that doesn't exist
user_override_cooldown:
name: Respect user overrides for
description: If you have overridden the automation (i.e. turned the HVAC back on or set the temperature manually after it was turned down), don't let environmental changes restart it for an amount of time. This is so that when you "turn the HVAC back up", you don't get it turned back down in a minute because the external temperature changed. Ignored if user overrides are not allowed
default:
hours: 6
minutes: 0
seconds: 0
selector:
duration:
script_turn_down:
name: Script for turning down HVAC
description: Script to run when the NVAC needs to be turned down as a result of an open window/door
selector:
entity:
domain:
- script
default: "a.b" # something that doesn't exist
script_turn_back_up:
name: Script for turning HVAC back up
description: Script to run in order to turn the HVAC back to its initial state as a result of the window/door closing
selector:
entity:
domain:
- script
default: "a.b" # something that doesn't exist
script_turn_back_up_override:
name: Script for turning HVAC back up (override)
description: Script to run to turn the HVAC back up as a result of a user clicking an action in the notification (user override)
selector:
entity:
domain:
- script
default: "a.b" # something that doesn't exist
script_notify:
name: Script for sending actionable notifications
description: Script to run in order to send notifications that the HVAC was turned down as a result of an open window/door
selector:
entity:
domain:
- script
default: "a.b" # something that doesn't exist
script_clear_notification:
name: Script for clearing notifications
description: Script for clearing notifications on mobiles in case the user hasn't interacted with them
selector:
entity:
domain:
- script
default: "a.b" # something that doesn't exist
mode: parallel # needed for multiple rooms
variables:
hvac: !input hvac
snapshot_scene: > # climate.some_thermostat -> scene.some_thermostat_snapshot
scene.{{hvac[8:]}}_snapshot
external_temperature: !input external_temperature
window_sensor: !input window_sensor
allow_user_override: !input allow_user_override
override_boolean: !input override_boolean
user_override_cooldown: !input user_override_cooldown
# whether the set temperature was lowered by the automation in order to save energy
energy_saving_active: >
{# last_changed gets updated upon scene activation, as the scene state gets updated. hence, to test if a scene was activated, the state and last_changed attribute need to be almost equal ("almost" due to race conditions) #}
{# the manual override is present if the scene was activated. the override is present if the scene has been activated #}
{# the energy saving is active if the snapshot scene exists, but has not been activated after creation. activation (as the last action) signifies override #}
{{ (states[snapshot_scene] is not none) and (is_state(snapshot_scene, 'unknown') or (states[snapshot_scene].last_changed.timestamp()|float(as_timestamp(now())) - as_timestamp(states(snapshot_scene))|float(as_timestamp(now())) > 0.1)) and not (is_state(override_boolean, 'on')) }}
user_override_active: >
{{ allow_user_override and is_state(override_boolean, 'on') }}
user_override_cooldown_seconds: >
{{ (user_override_cooldown.seconds + 60*user_override_cooldown.minutes + 3600*user_override_cooldown.hours)|float(0) }}
user_override_cooldown_active: >
{{ user_override_active and (as_timestamp(now()) - states[override_boolean].last_updated.timestamp())|float(0) < user_override_cooldown_seconds }}
script_turn_down: !input script_turn_down
script_turn_back_up: !input script_turn_back_up
script_turn_back_up_override: !input script_turn_back_up_override
script_notify: !input script_notify
script_clear_notification: !input script_clear_notification
trigger:
- platform: state
entity_id: !input window_sensor
to: "on"
for: !input on_debounce_time
id: "window_open"
- platform: state
entity_id: !input window_sensor
to: "off"
for: !input off_debounce_time
id: "window_closed"
- platform: state
entity_id: !input hvac
attribute: temperature
id: "set_temperature_changed"
- platform: state
entity_id: !input external_temperature
id: "ext_temperature_change"
- platform: homeassistant
event: shutdown
id: ha_restart
condition:
- condition: state
entity_id: !input hvac
state:
- heat
- cool
action:
- alias: "Reset override cooldown"
if:
- condition: template
value_template: "{{user_override_active and not user_override_cooldown_active}}"
then:
- service: input_boolean.turn_off
target:
entity_id: !input override_boolean
- choose:
- conditions:
- condition: trigger
id: ha_restart
sequence:
- service: "{{script_turn_back_up}}"
data:
hvac: !input hvac
snapshot_scene: "{{snapshot_scene}}"
override_boolean: "{{override_boolean}}"
- conditions:
- condition: trigger
id: set_temperature_changed
- condition: template
value_template: "{{states.script|selectattr('context.id', 'eq', context.parent_id)|selectattr('entity_id', 'in', script_turn_down)|list|length == 0}}" # temperature change was not done by this automation
- condition: template
value_template: "{{energy_saving_active}}"
- condition: template
value_template: "{{ (not allow_user_override) or states[override_boolean].state is defined }}" # if it exists, not if it's on
sequence:
- alias: "Reset override"
service: scene.create
data:
scene_id: "{{snapshot_scene[6:]}}" # exclude 'scene.'
snapshot_entities: "{{hvac}}"
- service: input_boolean.turn_on
target:
entity_id: !input override_boolean
- conditions:
- alias: "If window is open"
condition: state
entity_id: !input window_sensor
state: "on"
- alias: "Energy saving is not active (temp was not lowered)"
condition: template
value_template: "{{not energy_saving_active}}" # includes user_override_active condition
- alias: "No override was set or override cooldown has passed"
condition: template
value_template: "{{not user_override_cooldown_active}}" # includes user_override_active condition
- condition: template
value_template: >
{% set direction = {"heat": 1, "cool": -1}[states(hvac)] or 0 %}
{{ direction*(states(external_temperature)|float(0) - state_attr(hvac, 'temperature')|float(0)) < 0 }}
- condition: template
value_template: "{{trigger.id != 'set_temperature_changed'}}"
sequence:
- service: "{{script_turn_down}}"
data:
hvac: !input hvac
snapshot_scene: "{{snapshot_scene}}"
override_boolean: "{{override_boolean}}"
- service: "{{script_notify}}"
data:
window_name: "{{ state_attr(window_sensor, 'friendly_name')|lower }}"
action_script: "{{script_turn_back_up_override}}"
hvac: !input hvac
snapshot_scene: "{{snapshot_scene}}"
allow_user_override: "{{allow_user_override}}"
override_boolean: "{{override_boolean}}"
- conditions:
- alias: "If window is closed"
condition: state
entity_id: !input window_sensor
state: "off"
- condition: template
value_template: "{{trigger.id != 'set_temperature_changed'}}"
sequence:
- if:
- alias: "Energy saving enabled"
condition: template
value_template: "{{energy_saving_active}}"
then:
- service: "{{script_turn_back_up}}"
data:
hvac: !input hvac
snapshot_scene: "{{snapshot_scene}}"
override_boolean: "{{override_boolean}}"
- if:
- condition: template
value_template: "{{ states[script_clear_notification] is defined }}"
then:
- service: "{{ script_clear_notification }}"
data:
notification_tag: "open_window_{{hvac|replace('.', '_')}}"
script:
turn_down_hvac:
alias: HVACW - Turn down HVAC
description: Turn down a HVAC as a result of an open window
fields:
hvac:
name: HVAC thermostat
description: The climate component which controls the HVAC in the room/area
selector:
entity:
domain: climate
snapshot_scene:
name: Snapshot scene
description: A scene entity which holds the state of the HVAC prior to it being changed
selector:
entity:
domain: scene
override_boolean:
name: Override indicator (optional)
description: An input_boolean where to store whether the HVAC temperature was overridden while the door/window was open
selector:
entity:
domain: input_boolean
default: "a.b"
variables:
hvac: !input hvac
snapshot_scene: !input snapshot_scene
override_boolean: !input override_boolean
sequence:
- service: scene.create
data:
scene_id: "{{snapshot_scene[6:]}}" # exclude 'scene.'
snapshot_entities: "{{hvac}}"
##### THIS IS THE PART THAT YOU SHOULD MODIFY ######
- service: climate.set_preset_mode
data:
preset_mode: eco
target:
entity_id: "{{hvac}}"
##### /THIS IS THE PART THAT YOU SHOULD MODIFY ######
mode: restart
turn_hvac_back_up:
alias: HVACW - Turn HVAC back up
description: Turn a HVAC back up as a result of a window being closed
fields:
hvac:
name: HVAC thermostat
description: The climate component which controls the HVAC in the room/area
selector:
entity:
domain: climate
snapshot_scene:
name: Snapshot scene
description: A scene entity which holds the state of the HVAC prior to it being changed
selector:
entity:
domain: scene
override_boolean:
name: Override indicator (optional)
description: An input_boolean where to store whether the HVAC temperature was overridden while the door/window was open
selector:
entity:
domain: input_boolean
default: "a.b"
variables:
hvac: !input hvac
snapshot_scene: !input snapshot_scene
override_boolean: !input override_boolean
sequence:
- service: scene.turn_on
target:
entity_id: "{{snapshot_scene}}"
mode: restart
turn_hvac_back_up_override:
alias: HVACW - Turn HVAC back up user override
description: Turn a HVAC back up as a result of a user overriding the automation
fields:
hvac:
name: HVAC thermostat
description: The climate component which controls the HVAC in the room/area
selector:
entity:
domain: climate
snapshot_scene:
name: Snapshot scene
description: A scene entity which holds the state of the HVAC prior to it being changed
selector:
entity:
domain: scene
override_boolean:
name: Override indicator (optional)
description: An input_boolean where to store whether the HVAC temperature was overridden while the door/window was open
selector:
entity:
domain: input_boolean
default: "a.b"
variables:
hvac: !input hvac
snapshot_scene: !input snapshot_scene
override_boolean: !input override_boolean
sequence:
- if:
- condition: template
value_template: "{{ states[override_boolean].state is defined }}"
then:
- service: input_boolean.turn_on
target:
entity_id: "{{override_boolean}}"
- service: notify.family
data:
message: clear_notification
data:
tag: "open_window_{{hvac|replace('.', '_')}}"
- service: script.turn_hvac_back_up
data:
hvac: "{{hvac}}"
snapshot_scene: "{{snapshot_scene}}"
override_boolean: "{{override_boolean}}"
mode: restart
notify_hvac_modified:
alias: HVACW - Notify that the HVAC was modified
description: Notify that a HVAC was turned down due to an open window/door and ask for confirmation to turn it back up or that it was turned up due to a window closing
fields:
window_name:
name: Window name
description: The name of the open window which triggered the notification
selector:
text:
allow_user_override:
name: Allow user override
description: Show action in notification which allows user override
default: false
selector:
boolean:
action_script:
name: Action script
description: The script to call if the user taps the action in the notification
selector:
entity:
domain:
- script
hvac:
name: HVAC thermostat
description: The climate component which controls the HVAC in the room/area
selector:
entity:
domain: climate
snapshot_scene:
name: Snapshot scene
description: A scene entity which holds the state of the HVAC prior to it being changed
selector:
entity:
domain: scene
override_boolean:
name: Override indicator (optional)
description: An input_boolean where to store whether the HVAC temperature was overridden while the door/window was open. This is passed to the relevant script
selector:
entity:
domain: input_boolean
default: "a.b"
variables:
window_name: !input window_name
allow_user_override: !input allow_user_override
action_script: !input action_script
hvac: !input hvac
snapshot_scene: !input snapshot_scene
override_boolean: !input override_boolean
sequence:
- alias: "Set up variables"
variables:
action_id: "{{ 'HVAC_MODIFIED_' ~ context.id }}"
##### THIS IS THE PART YOU SHOULD MODIFY #####
- if:
- alias: "Nobody is home"
condition: state
entity_id: zone.home
state: "0"
then:
- service: script.send_hvac_notification
data:
notify_service: "notify.family"
notification_message: "The {{ state_attr(hvac, 'friendly_name')|lower }} was turned down because the {{ window_name }} is open. Would you like to turn it back up?"
notification_tag: "open_window_{{hvac|replace('.', '_')}}"
action_needed: "{{allow_user_override}}}"
action_id: "{{action_id}}"
action_title: "Turn it back up"
# the URL should be used if you have room-based dashboards
action_url: >
/lovelace-rooms/{{ hvac|regex_replace('climate\.([a-z_]+)_floor_heating', '\\1') }}
else:
- if:
- alias: "Send messages to those who are home"
condition: state
entity_id: person.your_name
state: home
then:
- service: script.send_hvac_notification
data:
notify_service: "notify.your_name"
notification_message: "The {{ state_attr(hvac, 'friendly_name')|lower }} was turned down because the {{ window_name }} is open. Would you like to turn it back up?"
notification_tag: "open_window_{{hvac|replace('.', '_')}}"
action_needed: "{{allow_user_override}}"
action_id: "{{action_id}}"
action_title: "Turn it back up"
# the URL should be used if you have room-based dashboards
action_url: >
/lovelace-rooms/{{ hvac|regex_replace('climate\.([a-z_]+)_floor_heating', '\\1') }}
##### /THIS IS THE PART YOU SHOULD MODIFY #####
- if:
- condition: template
value_template: "{{allow_user_override}}"
then:
- alias: "Awaiting response"
wait_for_trigger:
- platform: event
event_type: mobile_app_notification_action
event_data:
action: "{{ action_id }}"
timeout: "06:00:00"
- if:
- condition: template
value_template: "{{ False if (wait.trigger is None) else (wait.trigger.event.data.action == action_id) }}"
then:
- service: "{{action_script}}"
data:
hvac: "{{hvac}}"
snapshot_scene: "{{snapshot_scene}}"
override_boolean: "{{override_boolean}}"
mode: restart
send_hvac_notification:
alias: HVACW - Send HVAC modification notification
description: actually send the notification
fields:
notify_service:
name: Notify service
description: The notify service to use (i.e. who should be getting the notification)
selector:
entity:
domain: notify
notification_message:
name: Notification message
description: The text of the notification message
selector:
text:
notification_tag:
name: Notification tag
description: The tag used for the companion app notification
selector:
text:
action_needed:
name: Include action
description: Whether an action button should be attached to the notification
selector:
boolean:
action_id:
name: Action ID
description: The action ID that should be sent as an event if the user clicks the action
action_title:
name: User action title
description: The button label that will be on the notification, which the user can click to trigger an action
action_url:
name: Action URL
description: The URL to go to when the user clicks the action
variables:
notify_service: !input notify_service
notification_message: !input notification_message
notification_tag: !input notification_tag
action_needed: !input action_needed
action_id: !input action_id
action_title: !input action_title
action_url: !input action_url
sequence:
- if:
- condition: template
value_template: "{{action_needed}}"
then:
- service: "{{notify_service}}"
data:
message: "{{notification_message}}"
data:
tag: "{{notification_tag}}"
actions:
- action: "{{action_id}}"
title: "{{action_title}}"
sticky: true
url: "{{action_url}}"
clickAction: "{{action_url}}"
else:
- service: "{{notify_service}}"
data:
message: "{{notification_message}}"
data:
tag: "{{notification_tag}}"
sticky: true
url: "{{action_url}}"
clickAction: "{{action_url}}"
clear_hvac_modified_notifications:
alias: HVACW - Clear HVAC notifications
description: Optionally clear notifications about energy saving on mobile terminals
fields:
notification_tag:
name: Notification tag
description: The tag used for the companion app notification
selector:
text:
variables:
notification_tag: !input notification_tag
sequence:
##### THIS IS THE PART YOU SHOULD MODIFY #####
- service: "notify.family"
data:
message: clear_notification
data:
tag: "{{notification_tag}}"
##### /THIS IS THE PART YOU SHOULD MODIFY #####
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment