Smart Climate Schedule on a Z-Wave Thermostat

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:

  1. The T2000 had no form of Nest’s “eco mode,” which alters the thermostat to stay within a range while no one is home.
  2. 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 the proximity 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: a min_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 to heat)
  • 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. :smile:

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!

5 Likes

Hi, it seems you’re doing something similar - maybe you can help me. I know in principle what I want but YAML is just a book with 7 seals for me.
I have an air conditioning (a/c) unit and I’d like to have an alarm when the a/c is switched on and the living room balcony door is still open.
The HA climate integration provides me an entity readout of the status of the a/c, combined with its temperature. I’ve tried everything to check only for the a/c status (basically this automation should run whenever the a/c is changing to any status but off). Is there a way to query whether the a/c is ‘not off’?

This is my climate.ac entity overview:

This is what the entity card displays if I display the climate.ac entity
ac2

This is the automation I’ve written (which doesn’t work). Any ideas? Help would be greatly appreciated. Have googled and tried for 2 weeks now, without success… (The action at the end of the code, toggling a lamp, is only an example)

alias: TEST
description: ''
trigger:
  - platform: state
    entity_id: climate.ac
    to: cool
  - platform: state
    entity_id: climate.ac
    to: heat
  - platform: state
    entity_id: climate.ac
    to: fan_only
  - platform: state
    entity_id: climate.ac
    to: dry
condition:
  - condition: or
    conditions:
      - condition: state
        entity_id: binary_sensor.doorsensor_eltern_access_control_window_door_is_open
        state: Open
        for:
          hours: 0
          minutes: 0
          seconds: 1
          milliseconds: 0
      - condition: state
        entity_id: binary_sensor.doorsensor_wz_access_control_window_door_is_open
        state: Open
        for:
          hours: 0
          minutes: 0
          seconds: 1
          milliseconds: 0
action:
  - type: toggle
    device_id: f0f676fed5df5de27d5bf9a7b7afa17e
    entity_id: switch.stehlampe_1
    domain: switch
mode: single

First, I would create a group for your windows; by default in a group of binary sensors, if one sensor is on, the group is on. This will make our automation simpler.

group:
  windows:
    entities:
      - binary_sensor.doorsensor_eltern_access_control_window_door_is_open
      - binary_sensor.doorsensor_wz_access_control_window_door_is_open

Then, you can write this automation:

---
alias: TEST

description: ''

trigger:
  - platform: state
    entity_id: climate.ac

  - platform: state
    entity_id: group.windows
    to: "on"

condition:
  - condition: "{{ trigger.to_state.state != trigger.from_state.state }}"

  - condition: "{{ not is_state('climate.ac', 'off') }}"

  - condition: state
    entity: group.windows
    state: "on"

action:
  - type: toggle
    device_id: f0f676fed5df5de27d5bf9a7b7afa17e
    entity_id: switch.stehlampe_1
    domain: switch

mode: single

Here’s what it does:

  1. The automation triggers anytime climate.ac changes state or anytime group.windows turns on (which, again, ostensibly means at least one binary sensor in that group turns on).
  2. To continue, the automation must pass three conditions:
    1. The automation trigger’s state itself (i.e., the value you see in HASS) needs to change. This is in place to ensure that the automation doesn’t trigger if, say, the thermostat is on and someone bumps it by a few degrees.
    2. climate.ac needs to have a state other than off.
    3. group.windows needs to be on.
  3. Execute the switch toggle.

Hi Aaron thanks for your help and also the great inputs which made me learn a lot (e.g. I wasn’t aware the trigger condition also works with no ‘to’ or ‘from’ value and agree: using a group is a good idea for such a function as I may purchase additional window/door sensors in the future - as you see I’ve called my group group.doors as I’m currently only using the sensor for my balcony doors in 2 floors). I’ve struggled a little bit with the syntax of the conditions part as I needed to make a few changes to get your code working for me 1:1. My question here: Doesn’t the

- condition: "{{ trigger.to_state.state != trigger.from_state.state }}"

need a further reference (i.e. trigger.to and trigger.from status of what?)? I’ve nevertheless not needed this part as I’ve found out that a change of the temperature setting doesn’t change the climate.ac state.

I have arrived at (this is only the trigger + condition section)…

trigger:
  - platform: state
    entity_id: climate.ac
  - platform: state
    entity_id: group.doors
    to: 'on'
condition:
  - condition: template
    value_template: "{{ not is_state('climate.ac', 'off') }}"
  - condition: state
    entity_id: group.doors
    state: 'on'

…but it somehow also doesn’t work for me (I may have introduced an error).

I have nevertheless kept playing with the code and (also thanks to your much appreciated input) ended up with this code which now finally works (again, this is only the trigger + condition section). I have also introduced the 8 seconds wait time for the doors to be open as I don’t want to get an alarm for the cases where someone quickly opens and closes a door.

trigger:
  - platform: state
    entity_id: climate.ac
  - platform: state
    entity_id: group.doors
    to: 'on'
    for:
      hours: 0
      minutes: 0
      seconds: 8
      milliseconds: 0
condition:
  - condition: not
    conditions:
      - condition: state
        entity_id: climate.ac
        state: 'off'
  - condition: state
    entity_id: group.doors
    state: 'on'

All in all, I’m really happy this now finally works. Thanks again for your help, really appreciated!

Glad to hear you got something in place!

That’s a fair point. That will technically respond to any trigger. It won’t materially impact anything, but you could theoretically add a trigger.entity_id == expression to make it more explicit.

Smart! FYI, yours is totally fine, but you can also do this:

  - platform: state
    entity_id: group.doors
    to: 'on'
    for:
      seconds: 8