Best Practice

I have an automation that I need to make more robust. It fails following a restart and I need help figuring out how best to work around or remedy the failure.

BACKGROUND: I have hot water heat at home with old mercury switches on 5 different zones. I’ve integrated two Zooz ZEN16s so that the relays act as thermostats, wired in parallel with each of the mercury switches. The mercury switches are left at about 50-55 deg F so that if HA fails, then they will kick on if the temp falls to that point, as a fail-over. Other than control via HA, I also wanted to automate a manual run in each zone if the outside temp falls below freezing and the zone hasn’t turned on in at least 2 hours. This is to prevent pipes from freezing in zones that may get heat from lower floors and may not actually trigger the thermostat to turn that zone on. I create the following automation, which works, except when following a restart, a zone never comes on at least once due to its own thermostat. The problem is the trigger, because I’m using from: "on", but following a restart, it may never come on to begin with, and the trigger will never fire, even if it’s been 2 hours and it is below freezing.

I need help with a trigger. I have other automations that have difficulty with the state of things like this following a restart. I’m curious if anyone has found a creative solution. I thought about having an automation that just toggles on/off the relays upon a restart, but that seems clunky, especially in the Summer. Any advice would be appreciated.

CURRENT AUTOMATION:

  # Cycle Heat Zones if Outside Temp Below Freezing and Zone Hasn't Been ON in a Set timeout
  - id: cycle_heat_zones_atm
    alias: Cycle Heat Zones (ATM)
    mode: parallel
    trigger:
      - platform: state
        entity_id:
          - switch.zooz_zen16_r1s1_tstat_ds
          - switch.zooz_zen16_r1s2_tstat_lr
          - switch.zooz_zen16_r1s3_tstat_mbr
          - switch.zooz_zen16_r2s1_tstat_us
          - switch.zooz_zen16_r2s2_tstat_gar
        from: "on"
        for: "02:00:00"
    condition:
      - condition: template
        value_template: "{{ (states('sensor.sensative_strips_comfort_temperature') | float < 32) or (states('sensor.zooz_zse40_multi_shed_temperature') | float < 32) }}"
    action:
      - service: homeassistant.turn_on
        data:
          entity_id: "{{ trigger.entity_id }}"

The challenge with that is it doesn’t start the 2-hour countdown until it first detects a state-change (from on to some other state such as off or even unavailable). As you explained, a restart wipes the slate clean and the trigger must first wait for a state-change before it starts the countdown.

The other bad news is that even just Reloading Automations will also wipe the slate clean. The trigger might be halfway through its countdown but if you use the Automation Editor to modify an automation, the moment you click Save it performs a Reload Automations.

One alternative is the concept of reporting “stale” entities (entity whose state value has remained unchanged for a given period of time).

For example, this template lists all switches whose state value hasn’t changed for more than 2 hours.

{{ expand('switch.zooz_zen16_r1s1_tstat_ds', 'switch.zooz_zen16_r1s2_tstat_lr',
       'switch.zooz_zen16_r1s3_tstat_mbr', 'switch.zooz_zen16_r2s1_tstat_us',
        'switch.zooz_zen16_r2s2_tstat_gar')
   | selectattr('last_changed', 'lt', now()-timedelta(hours=2))
   | map(attribute='entity_id') | join(', ') }}

It could be used to create a Template Sensor and an automation can employ it in a State Trigger.

However, even this technique has a vulnerability. On startup, Home Assistant resets the last_changed property to the current time: any elapsed time is lost on startup. The advantage is that this technique isn’t affected by a Reload Automations and it will start calculating elapsed time immediately after a restart.

1 Like

I think I also see another challenge: What will happen if after 2 hours of a switch being off, and the temp is 33, so nothing happens due to the condition. Now if the temp drops to 31, nothing will ever happen, until the zone is turned on by something else, or a restart.
I suggest an alternate way to do this is along these lines:
Make an automation that sets an input_datetime for each switch if its ‘on’, time trigger every minute or 5, or 10, whatever works for you. These will survive a restart if no intial: value is set, so now you know when each pump was last on.
Now call for service to turn on each switch, with 2 conditions: 1 using a value_template that compares the input_datetime for each switch to (now), if delta is greater than 2 hours, and a condition of outside temp less than 32 degrees(like you have), then turn on the switch. Add a delay for how long to keep the pump on, then turn it off. You probably want to incorporate a condition here to not turn it off if the zone is calling for heat, in the case that the automation started the pump, but now the room wants heat, before the delay is up. Just a possible way to go about it, and probably can be streamlined

These are indeed interesting ideas and provides me with details that will help now and into the future with other automations as well. Thank you for taking the time to respond in such detail with excellent examples. I may give all of part of this a try in a solution. Cheers.

Ah, you are right and I discovered that last night after having turned them all on briefly when it was below freezing. As long as it stayed below, the automation worked, but as soon as it went above freezing, it never worked again, even as the temp dropped again,. As you pointed out, a hole in the trigger.

I like your idea of a separate entity to keep track of information that HA normally won’t track accurately following a restart or reloads of the automations. I actually use something similar to this already in another instance where I use a Zooz Kit with a battery-powered door sensor as a ZWave device that is connected to a DC transformer plugged into an outlet. Whenever the sensor detects that the power is off, it sends a notification to HA, which lets me know the power is off. However, the state still showed OFF in HA because when the power came back on, the sensor would send an update, but HA missed it because it was booting up (when the outage was long enough to require powering down the hub - on short outages it worked fine). So I had to create an input boolean that tracked things appropriately across outages, using the initial_state customization.

I like this solution too and will give it a try. Maybe in the future there will be additional attributes for entities that survive a restart (like the previous state does now) to use for instances like this where we want to track things across restarts or changes to automations. That last bit throws a kink in my light automation that I’ve had to be creative to work around every time I reload automations. I added automation_reloaded to the trigger with mode: restart so that it restarts that particular automation upon reloads. Seems to work. Cheers.

where do you find the documdenation on how to use this filter. The jinja docs don’t explain it well, and I would have never guessed you could do what you did. I’ve also tried to do the test you have using states_attr(), but can’t get it to work. It’s thinks there is no value for ‘last_changed’ in that function.

https://jinja2docs.readthedocs.io/en/stable/templates.html#selectattr

The state_attr() function is for accessing the contents of a State Object’s attributes property. What everyone commonly refers to as an “attribute” is actually an item within the attributes property.

last_changed is a property of a State Object, not an item within the attributes property. That’s why the state_attr() function cannot use it.

For more information, refer to the documentation for State Objects. It explains each property of a State Object and makes it easier to understand how to get their values.

Thanks. I actually had read both of those docs multiple times, but neither explained how you knew to do the following:

selectattr('last_changed', 'lt', now()-timedelta(hours=2)).

And what kind of object does it work on? A list? It is only a filter, right, and can it be used stand-alone, and if so, how?

I can do the following:

{{ as_local(states.switch.zooz_zen16_r2s1_tstat_us.last_changed) }}

but the docs say to avoid that syntax, and I can’t figure out how to get it the recommended way: states(). I’m trying to build that sensor with the last_changed time.

You might be confusing what these two do:

state_attr

selectattr

The first is unique to Home Assistant and is for accessing Home Assistant’s entity attributes.

The second is native to Jinja2 and the attr in selectattr has nothing to do with Home Assistant’s entity attributes. It’s for accessing elements of the data structure received by selectattr. For example, this is not referring to an entity attribute called entity_id but an attribute/element of the data being passed to it.

| map(attribute='entity_id')

Yes, the data passed to it could be a list or a dict (or a generator object, but let’s skip that for now).

The documentation refers to getting an entity’s state value. The preference is to do this:

states('light.whatever')

as opposed to this:

states.light.whatever.state

because the states() function can gracefully handle a non-existent entity whereas the second form cannot.

However, there’s no function to get the value of an entity’s last_changed property so, in this case, you have to use the second form.

Ah, OK. Thanks for the details. The lines between Jinja template engine and HA ‘extensions’ is blurry for me, clearly.

OK, just for completeness, here is what I came up with. It appears to be working so far. If you see any holes, please let me know.

My input_datetime parameters to store the last_updated time for each zone.

input_datetime:
  # Thermostat Downstairs
  thermostat_ds_idt:
    name: Last Changed - Thermostat Downstairs (IDT)
    has_date: true
    has_time: true
  # Thermostat Living Room
  thermostat_lr_idt:
    name: Last Changed - Thermostat Living Room (IDT)
    has_date: true
    has_time: true
  # Thermostat Master Bedroom
  thermostat_mbr_idt:
    name: Last Changed - Thermostat Master Bedroom (IDT)
    has_date: true
    has_time: true
  # Thermostat Upstairs
  thermostat_us_idt:
    name: Last Changed - Thermostat Upstairs (IDT)
    has_date: true
    has_time: true
  # Thermostat Garage
  thermostat_gar_idt:
    name: Last Changed - Thermostat Garage (IDT)
    has_date: true
    has_time: true

My Triggered Template Binary Sensors that turn on when I need to cycle the zone

template:
  - trigger:
    - platform: state
      entity_id: input_number.stale_zone_override_time_in
    - platform: state
      entity_id: input_number.temp_to_cycle_stale_heat_zones_in
    - platform: time_pattern
      minutes: "/5"
  # Sensors to track Stale Zone operations (based upon input_number.stale_zone_override_time_in)
  # Must convert input_number.stale_zone_override_time_in to SECONDS in templates
    binary_sensor:
      # Downstairs
      - name: Stale Zone Downstairs
        state: >
          {{ 
            ((as_timestamp(now()) - as_timestamp(states('input_datetime.thermostat_ds_idt'))) > 
              states('input_number.stale_zone_override_time_in') | default(240) | float * 60) and
              ((states('sensor.sensative_strips_comfort_temperature') | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float) or 
              (states('sensor.zooz_zse40_multi_shed_temperature')  | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float)) 
          }}
        unique_id: stale_zone_ds
        icon: mdi:fire
        device_class: heat
      # Living Room
      - name: Stale Zone Living Room
        state: >
          {{ 
            ((as_timestamp(now()) - as_timestamp(states('input_datetime.thermostat_lr_idt'))) > 
              states('input_number.stale_zone_override_time_in') | default(32) | default(240) | float * 60) and 
              ((states('sensor.sensative_strips_comfort_temperature') | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float) or 
              (states('sensor.zooz_zse40_multi_shed_temperature')  | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float)) 
          }}
        unique_id: stale_zone_lr
        icon: mdi:fire
        device_class: heat
      # Master Bedroom
      - name: Stale Zone Master Bedroom
        state: >
          {{ 
            ((as_timestamp(now()) - as_timestamp(states('input_datetime.thermostat_mbr_idt'))) > 
              states('input_number.stale_zone_override_time_in') | default(240) | float * 60) and 
              ((states('sensor.sensative_strips_comfort_temperature') | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float) or 
              (states('sensor.zooz_zse40_multi_shed_temperature')  | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float)) 
          }}
        unique_id: stale_zone_mbr
        icon: mdi:fire
        device_class: heat
      # Upstairs
      - name: Stale Zone Upstairs
        state: >
          {{ 
            ((as_timestamp(now()) - as_timestamp(states('input_datetime.thermostat_us_idt'))) > 
              states('input_number.stale_zone_override_time_in') | default(240) | float * 60) and 
              ((states('sensor.sensative_strips_comfort_temperature') | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float) or 
              (states('sensor.zooz_zse40_multi_shed_temperature')  | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float)) 
          }}
        unique_id: stale_zone_us
        icon: mdi:fire
        device_class: heat
      # Garage
      - name: Stale Zone Garage
        state: >
          {{ 
            ((as_timestamp(now()) - as_timestamp(states('input_datetime.thermostat_gar_idt'))) > 
              states('input_number.stale_zone_override_time_in') | default(240) | float * 60) and 
              ((states('sensor.sensative_strips_comfort_temperature') | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float) or 
              (states('sensor.zooz_zse40_multi_shed_temperature')  | float < states('input_number.temp_to_cycle_stale_heat_zones_in') | default(32) | float)) 
          }}
        unique_id: stale_zone_gar
        icon: mdi:fire
        device_class: heat

And my automations to set the last_update time for each zone

  # Set Zone Last Update Downstairs
  - id: set_zone_last_update_ds_atm
    alias: Set Zone Last Update Downstairs (ATM)
    trigger:
      - platform: state
        entity_id: switch.zooz_zen16_r1s1_tstat_ds
        from: 'on'
    action:
      - service: input_datetime.set_datetime
        data:
          datetime: "{{ states.switch.zooz_zen16_r1s1_tstat_ds.last_changed.astimezone() }}"
        target:
          entity_id: input_datetime.thermostat_ds_idt
      - service: logbook.log
        data:
          name: Heat
          message: "Setting last_updated for {{ state_attr(trigger.entity_id,'friendly_name') }} Time={{ states('input_datetime.thermostat_ds_idt') }}"
  # Set Zone Last Update Living Room
  - id: set_zone_last_update_lr_atm
    alias: Set Zone Last Update Living Room (ATM)
    trigger:
      - platform: state
        entity_id: switch.zooz_zen16_r1s2_tstat_lr
        from: 'on'
    action:
      - service: input_datetime.set_datetime
        data:
          datetime: "{{ states.switch.zooz_zen16_r1s2_tstat_lr.last_changed.astimezone() }}"
        target:
          entity_id: input_datetime.thermostat_lr_idt
      - service: logbook.log
        data:
          name: Heat
          message: "Setting last_updated for {{ state_attr(trigger.entity_id,'friendly_name') }} Time={{ states('input_datetime.thermostat_lr_idt') }}"
  # Set Zone Last Update Master Bedroom
  - id: set_zone_last_update_mbr_atm
    alias: Set Zone Last Update Master Bedroom (ATM)
    trigger:
      - platform: state
        entity_id: switch.zooz_zen16_r1s3_tstat_mbr
        from: 'on'
    action:
      - service: input_datetime.set_datetime
        data:
          datetime: "{{ states.switch.zooz_zen16_r1s3_tstat_mbr.last_changed.astimezone() }}"
        target:
          entity_id: input_datetime.thermostat_mbr_idt
      - service: logbook.log
        data:
          name: Heat
          message: "Setting last_updated for {{ state_attr(trigger.entity_id,'friendly_name') }} Time={{ states('input_datetime.thermostat_mbr_idt') }}"
  # Set Zone Last Update Upstairs
  - id: set_zone_last_update_us_atm
    alias: Set Zone Last Update Upstairs (ATM)
    trigger:
      - platform: state
        entity_id: switch.zooz_zen16_r2s1_tstat_us
        from: 'on'
    action:
      - service: input_datetime.set_datetime
        data:
          datetime: "{{ states.switch.zooz_zen16_r2s1_tstat_us.last_changed.astimezone() }}"
        target:
          entity_id: input_datetime.thermostat_us_idt
      - service: logbook.log
        data:
          name: Heat
          message: "Setting last_updated for {{ state_attr(trigger.entity_id,'friendly_name') }} Time={{ states('input_datetime.thermostat_us_idt') }}"
  # Set Zone Last Update Garage
  - id: set_zone_last_update_gar_atm
    alias: Set Zone Last Update Garage (ATM)
    trigger:
      - platform: state
        entity_id: switch.zooz_zen16_r2s2_tstat_gar
        from: 'on'
    action:
      - service: input_datetime.set_datetime
        data:
          datetime: "{{ states.switch.zooz_zen16_r2s2_tstat_gar.last_changed.astimezone() }}"
        target:
          entity_id: input_datetime.thermostat_gar_idt
      - service: logbook.log
        data:
          name: Heat
          message: "Setting last_updated for {{ state_attr(trigger.entity_id,'friendly_name') }} Time={{ states('input_datetime.thermostat_gar_idt') }}"

Final Automation to Cycle Zones

  # cycle heat
  - id: cycle_heat_zones_atm
    alias: Cycle Heat Zones (ATM)
    mode: parallel
    trigger:
      - platform: state
        entity_id:
          - binary_sensor.stale_zone_ds
          - binary_sensor.stale_zone_lr
          - binary_sensor.stale_zone_mbr
          - binary_sensor.stale_zone_us
          - binary_sensor.stale_zone_gar
        to: 'on'
    action:
      - variables:
          eid: >-
            {% if trigger.entity_id == "binary_sensor.stale_zone_ds" %}
              switch.zooz_zen16_r1s1_tstat_ds
            {% elif trigger.entity_id == "binary_sensor.stale_zone_lr" %}
              switch.zooz_zen16_r1s2_tstat_lr
            {% elif trigger.entity_id == "binary_sensor.stale_zone_mbr" %}
              switch.zooz_zen16_r1s3_tstat_mbr
            {% elif trigger.entity_id == "binary_sensor.stale_zone_us" %}
              switch.zooz_zen16_r2s1_tstat_us
            {% elif trigger.entity_id == "binary_sensor.stale_zone_gar" %}
              switch.zooz_zen16_r2s2_tstat_gar
            {% endif %}
      - service: logbook.log
        data:
          name: Heat
          message: "{{ state_attr(trigger.entity_id,'friendly_name') }} triggered zone cycle. Switch = {{ eid }}"
      - service: homeassistant.turn_on
        data:
          entity_id: "{{ eid }}"
      - delay:
          minutes: 4
      - service: homeassistant.turn_off
        data:
          entity_id: "{{ eid }}"
1 Like