Background
When Nest decided to strangle API access to its thermostat, my wife and I decided to move on. We landed on the ADC T2000: I liked the local control via Z-Wave (a protocol that is already widely deployed in our home) and she liked the clean aesthetic. Things seemed well.
However, I quickly realized two features that I was missing from the Nest:
- The T2000 had no form of Nest’s “eco mode,” which alters the thermostat to stay within a range while no one is home.
- Although we didn’t rely on it overmuch, we missed Nest’s scheduling mechanism – even if the “learning” aspect wasn’t crucial to reproduce, we hated only manually interacting with the thermostat.
I tinkered for a long time and finally landed on a pure-HASS solution (i.e., using only HASS automations, although the logic can easily be adapted to Node-RED, AppDaemon, etc.) that has been working well. It’s nothing earth-shattering – many of these concepts have been employed elsewhere – but wanted to share for inspiration.
Goals
My goals for the thermostat:
- Employ a basic scheduling mechanism that alters the thermostat’s target temperature based upon several criteria (presence, what time of day it is, etc.)
- Modify the thermostat’s target temperature based on (a) the outside temperature and (b) how far away we are from home
- Allow on-the-fly re-evaluation of the scheduling rules (so that overrides can be “reset” later on)
Normalizing our Proximity to Home
Our first objective was to have a quick, easy-to-understand value to determine how far away from home the nearest person is.
Entities
-
input_number.home_radius
: how far away from home should that person be (in feet) and still be considered at home? -
input_number.nearby_radius
: how far away from home should that person be (in feet) to be considered nearby? -
input_number.edge_radius
: how far away from home should that person be (in feet) to be considered at the edge of our “we’re close to home” range (before we’re officially “away”)? -
proximity.home
: how far away from home the nearest person is (in feet) (using theproximity
integration)
Using these entities, we create a template sensor (sensor.proximity_zone
) that spits out one of 4 values to indicate what “proximity zone” the nearest person is in:
Home
Nearby
Edge
Away
sensor:
- platform: template
sensors:
proximity_zone:
friendly_name: Zone
entity_id:
- input_number.edge_radius
- input_number.home_radius
- input_number.nearby_radius
- proximity.home
value_template: >
{% set prox_mi = states("proximity.home")|int / 5280 %}
{% set home = states("input_number.home_radius")|int %}
{% set nearby = states("input_number.nearby_radius")|int %}
{% set edge = states("input_number.edge_radius")|int %}
{% if prox_mi <= home %}
Home
{% elif home < prox_mi <= nearby %}
Nearby
{% elif nearby < prox_mi <= edge %}
Edge
{% else %}
Away
{% endif %}
icon_template: mdi:radius
Setting Away Mode (based on our proximity zone)
With the concept of sensor.proximity_zone
in place, we can create a handful of automations to set our “Away Mode” depending on (a) our proximity and (b) the outdoor temperature.
Entities
-
sensor.side_yard_temp
: the outdoor temperature at our house as measured by our Ambient Weather Station -
input_boolean.away_mode
: indicates whether the house is in “Away Mode” – we can toggle this directly and it turns on/off based on our presence/proximity -
input_number.outdoor_extreme_high_threshold
: a slider that allows us to configure what we constitute as an “extremely high” outdoor temperature -
input_number.outdoor_extreme_low_threshold
: a slider that allows us to configure what we constitute as an “extremely low” outdoor temperature
Automations
The first two automations handle setting “Away Mode” depending on the outdoor temperature:
automation:
- alias: "Set Away Mode when leaving in extreme temperatures"
trigger:
platform: state
entity_id: sensor.proximity_zone
to: Away
condition:
condition: template
value_template: >
{{
states("sensor.side_yard_temp") <= states(
"input_number.outdoor_extreme_low_threshold"
)
or
states("sensor.side_yard_temp") >= states(
"input_number.outdoor_extreme_high_threshold"
)
}}
action:
service: input_boolean.turn_on
data:
entity_id: input_boolean.away_mode
- alias: "Set Away Mode when leaving in normal temperatures"
trigger:
platform: state
entity_id: sensor.proximity_zone
from: Home
condition:
condition: template
value_template: >
{{
states("input_number.outdoor_extreme_low_threshold") <=
states("sensor.side_yard_temp") <=
states("input_number.outdoor_extreme_high_threshold")
}}
action:
service: input_boolean.turn_on
data:
entity_id: input_boolean.away_mode
The first automation handles setting away mode with extreme outdoor temperatures: the switch occurs only when we’ve entered the Away
proximity zone. The second automation handles setting away mode with normal outdoor temperatures. Similar idea, but in this case, we activate “Away Mode” sooner. Overall, we link “Away Mode” to (a) how far away we are and (b) how extreme the outdoor temperature is.
An additional two automations revert “Away Mode” appropriately, once again based on outdoor temperature:
automation:
- alias: "Unset Away Mode when arriving in extreme temperatures"
trigger:
platform: state
entity_id: sensor.proximity_zone
from: Away
condition:
condition: template
value_template: >
{{
states("sensor.side_yard_temp") <= states(
"input_number.outdoor_extreme_low_threshold"
)
or
states("sensor.side_yard_temp") >= states(
"input_number.outdoor_extreme_high_threshold"
)
}}
action:
service: input_boolean.turn_off
data:
entity_id: input_boolean.away_mode
- alias: "Unset Away Mode when arriving in normal temperatures"
trigger:
- platform: state
entity_id: sensor.proximity_zone
from: Edge
to: Nearby
- platform: state
entity_id: sensor.proximity_zone
from: Nearby
to: Home
condition:
condition: template
value_template: >
{{
states("input_number.outdoor_extreme_low_threshold") <=
states("sensor.side_yard_temp") <=
states("input_number.outdoor_extreme_high_threshold")
}}
action:
service: input_boolean.turn_off
data:
entity_id: input_boolean.away_mode
Notice that we haven’t touched anything explicitly related to the thermostat – “Away Mode” remains a generic mechanism that can work for the thermostat as well as any other automation.
The Thermostat
With “Away Mode” firmly established, we can create a master “Climate Schedule” automation that makes use of it.
Entities
To accomplish these goals, the following entities are utilized:
-
climate.thermostat
: the actual thermostat -
sensor.average_interior_temperature
: amin_max
entity that averages the values of several different temperature sensors around the house -
input_boolean.blackout_mode
: indicates that we’re winding down for the night and heading to bed; can be set manually or via automation (we have automations to set/unset this at certain times depending on whether its a weekday) -
input_number.thermostat_eco_high_threshold
: a slider that allows us to configure what the highest allowed inside temperature should be when we’re away -
input_number.thermostat_eco_low_threshold
: a slider that allows us to configure what the lowest allowed inside temperature should be when we’re away
EDIT: since the advent of the choose
action, these can be combined into a single automation:
automation:
- alias: "Proximity Climate Away Mode"
trigger:
platform: state
entity_id: sensor.proximity_zone
action:
choose:
# Arriving (Normal Temperature)
- conditions:
- condition: or
conditions:
- condition: state
entity_id: sensor.proximity_zone
state: Nearby
- condition: state
entity_id: sensor.proximity_zone
state: Home
- condition: template
value_template: >
{{
states("input_number.outdoor_extreme_low_threshold") <=
states("sensor.side_yard_feels_like") <=
states("input_number.outdoor_extreme_high_threshold")
}}
sequence:
service: input_boolean.turn_off
data:
entity_id: input_boolean.away_mode
# Leaving (Normal Temperature)
- conditions:
- condition: not
conditions:
- condition: state
entity_id: sensor.proximity_zone
state: Home
- condition: template
value_template: >
{{
states("input_number.outdoor_extreme_low_threshold") <=
states("sensor.side_yard_feels_like") <=
states("input_number.outdoor_extreme_high_threshold")
}}
sequence:
service: input_boolean.turn_on
data:
entity_id: input_boolean.away_mode
# Arriving (Extreme Temperature)
- conditions:
- condition: not
conditions:
- condition: state
entity_id: sensor.proximity_zone
state: Away
- condition: template
value_template: >
{{
states(
"sensor.side_yard_feels_like"
) <= states(
"input_number.outdoor_extreme_low_threshold"
)
or states(
"sensor.side_yard_feels_like"
) >= states(
"input_number.outdoor_extreme_high_threshold"
)
}}
sequence:
service: input_boolean.turn_off
data:
entity_id: input_boolean.away_mode
# Leaving (Extreme Temperature)
- conditions:
- condition: state
entity_id: sensor.proximity_zone
state: Away
- condition: template
value_template: >
{{
states(
"sensor.side_yard_feels_like"
) <= states(
"input_number.outdoor_extreme_low_threshold"
)
or states(
"sensor.side_yard_feels_like"
) >= states(
"input_number.outdoor_extreme_high_threshold"
)
}}
sequence:
service: input_boolean.turn_on
data:
entity_id: input_boolean.away_mode
The Scheduler
We use a single automation (that uses the new choose
logic) for climate scheduling:
automation:
- alias: "Climate Schedule"
mode: queued
trigger:
- platform: homeassistant
event: start
# We explicitly watch for state changes where the state itself changes;
# this prevents manually adjustments to the thermostat from triggerig
# this automation:
- platform: state
entity_id: climate.thermostat_mode
from: heat_cool
- platform: state
entity_id: climate.thermostat_mode
from: cool
to: heat
- platform: state
entity_id: climate.thermostat_mode
from: heat
to: cool
- platform: state
entity_id: climate.thermostat_mode
from: "off"
to: cool
- platform: state
entity_id: climate.thermostat_mode
from: "off"
to: heat
- platform: state
entity_id: input_boolean.blackout_mode
- platform: state
entity_id: input_boolean.away_mode
- platform: state
entity_id: input_number.thermostat_eco_high_threshold
- platform: state
entity_id: input_number.thermostat_eco_low_threshold
action:
choose:
# Away Mode On
- conditions:
condition: state
entity_id: input_boolean.away_mode
state: "on"
sequence:
- service: input_select.select_option
data:
entity_id: input_select.last_hvac_mode
option: "{{ states('climate.thermostat_mode') }}"
- service: climate.set_hvac_mode
data:
entity_id: climate.thermostat_mode
hvac_mode: heat_cool
- wait_template: >
{{ is_state("climate.thermostat_mode", "heat_cool") }}
- service: climate.set_temperature
data:
entity_id: climate.thermostat_mode
target_temp_high: >
{{ states("input_number.thermostat_eco_high_threshold")|int }}
target_temp_low: >
{{ states("input_number.thermostat_eco_low_threshold")|int }}
# Away Mode Off
- conditions:
- condition: state
entity_id: climate.thermostat_mode
state: heat_cool
- condition: state
entity_id: input_boolean.away_mode
state: "off"
sequence:
service: climate.set_hvac_mode
data:
entity_id: climate.thermostat_mode
hvac_mode: "{{ states('input_select.last_hvac_mode') }}"
# A/C Daytime
- conditions:
- condition: state
entity_id: climate.thermostat_mode
state: cool
- condition: state
entity_id: input_boolean.blackout_mode
state: "off"
- condition: state
entity_id: input_boolean.away_mode
state: "off"
sequence:
service: climate.set_temperature
entity_id: climate.thermostat_mode
data:
temperature: 75
# A/C Nighttime
- conditions:
- condition: state
entity_id: climate.thermostat_mode
state: cool
- condition: state
entity_id: input_boolean.blackout_mode
state: "on"
- condition: state
entity_id: input_boolean.away_mode
state: "off"
sequence:
service: climate.set_temperature
entity_id: climate.thermostat_mode
data:
temperature: 72
# Heat Daytime
- conditions:
- condition: state
entity_id: climate.thermostat_mode
state: heat
- condition: state
entity_id: input_boolean.blackout_mode
state: "off"
- condition: state
entity_id: input_boolean.away_mode
state: "off"
sequence:
service: climate.set_temperature
entity_id: climate.thermostat_mode
data:
temperature: 70
# Heat Nighttime
- conditions:
- condition: state
entity_id: climate.thermostat_mode
state: heat
- condition: state
entity_id: input_boolean.blackout_mode
state: "on"
- condition: state
entity_id: input_boolean.away_mode
state: "off"
sequence:
service: climate.set_temperature
entity_id: climate.thermostat_mode
data:
temperature: 68
Triggers
There are many triggers that cause a re-evaluation of this schedule (i.e., a re-running of the automation). In principle, we want this schedule to be evaluated when:
- HASS starts
- We change the thermostat’s mode (e.g., from
cool
toheat
) - We enter “Blackout Mode”
- We enter “Away Mode”
- We alter the highest- our lowest-allowed interior temperatures
- We hear the
EVALUATE_CLIMATE
event (more on that shortly)
Actions
The actions
block indicates what we should do, when. Right now, we have 3 possible states for both A/C and heating:
- When we’re in “Away Mode,” set the temperature to maximum (or minimum, depending on whether we’re heating or cooling) allowed temperature.
- When we’re home during the day (e.g., “Blackout Mode” is inactive), set a sane temperature.
- When we’re at home during the night (e.g., “Blackout Mode” is active), set a temperature comfortable for sleeping
I mentioned the EVALUATE_CLIMATE
event – this mechanism allows us to dynamically (and on the fly) force a re-evaluation of this climate schedule. Any automation can fire this event, which means that any automation can “suggest” a climate reset without directly knowing about the thermostat. We’re still playing with use cases here, but we’re leaning towards “reset” logic: if someone messes with the thermostat, perhaps we fire this event once an hour to get it back on track. We’ll see.
Summary
Overall, this is a fairly robust, nicely abstracted system that allows us to very intelligently control our climate with a relatively low amount of work, all directly using HASS automations. It doesn’t “learning” capabilities of the Nest, nor does it have the Nest’s calendar UI for scheduling temperatures (although one could envision extending this with more HASS UI elements to construct such a mechanism), but it gets the job done.
I also like the logical separation of things – if we ever want to tie additional functionality to, say, “Away Mode,” we can do so because it isn’t inextricably linked to our thermostat.
Hope this is instructive (or at least fun). Thanks!