Automation recovery after HA restart

So I have an automation that locks my front door with a trigger that kicks in after the lock went from lock to unlock for 30mins. The issue is, if I restart HA within that timeframe, it kills the automation and the door never locks. Is there a way to retains the state of entities during restarts so that the automation can pick up where it left off?

No, your State Trigger is using the for option to countdown 30 minutes and that’s lost after a restart. The same thing happens to delay, wait_template, wait_for_trigger, and timers. Just executing Reload Automations will also reset them (except timers).

What does survive a restart is a counter but then you have to implement the automation that controls it (i.e. performs the actual countdown that decrements the counter every minute then resets it to 30 after in decreases to 0).

An alternative is to use a timer but with something I created to automatically restore active timers after a restart.

The advantage of using a timer vs a counter is that the timer takes care of decrementing itself. However, both require that you create an automation that is triggered when the lock changes state to unlocked and then starts the timer (or begins decrementing the counter). Once the timer expires (or the counter decreases to zero) an automation is triggered and performs whatever action you require.

If your first impression is “That’s a lot more work than simply using the for option!”, you’re not wrong. The for option is very convenient but, as you know, doesn’t survive a restart or a Reload Automations.

2 Likes

Thanks for this info. I was actually using timers for this when I used HomeSeer, but this required three separate automations, one to lock the door (if the timer expires and the door is unlocked), one to start the timer when the door unlocked and a third to reset the timer if the door was manually locked.

My idea of using for was to have this in a single automation, but didn’t realize the state time didn’t survive a restart. This probably explains why the last-changed and last-updated values for the entities on my Lovelace dashboard gets reset after a HA restart. Very inconvenient.

Would that be possible in HA? To have all of this timer options in a single automation?

This assumes you have lock.door and timer.door_lock entities (change their names to match yours).

alias: Automatic Door Lock
id: automatic_door_lock
mode: queued
trigger:
- platform: state
  entity_id: lock.door
- platform: event
  event_type: timer.finished
  event_data:
    entity_id: timer.door_lock
action:
- choose:
  - conditions: "{{ trigger.platform == 'state' }}"
    sequence:
    - service: "timer.{{ 'start' if trigger.to_state.state == 'unlocked' else 'cancel' }}"
      target:
        entity_id: timer.door_lock
  - conditions:
    - "{{ trigger.platform == 'event' }}"
    - "{{ is_state('lock.door', 'unlocked') }}"
    sequence:
    - service: lock.lock
      target:
        entity_id: lock.door

NOTE
Currently it confirms the door lock is unlocked before locking it (to avoid needlessly issuing a lock command). One additional check it can perform is to ensure the door is actually closed (by checking the state of the door’s contact sensor, assuming there is one) before attempting to lock the door.

Also, don’t forget that the timer won’t survive a restart unless you implement that workaround I posted to restore active timers.

the other (most safe) option is to set an input_datetime to 30 minutes from the time the lock goes to unlocked - so unlocked time + 30 minutes.

then use another automation to trigger at the time value of the input_datetime above to lock the door.

Messing with timers, waits, for, etc for important things like door locks is not the best way to do things.

4 Likes

I like your idea so much that I think it should become the default design pattern for situations requiring a durable replacement for the for option. :+1:


You inspired me to explore your suggestion and create an automation for it.

The input_datetime should include both date and time because a time-only version would trigger the following day.

It’s easy to set the input_datetime’s value to the current time plus 30 minutes.

{{ now() + timedelta(minutes = 30) }}

However, input_datetime.set_datetime requires that the result be a time string in this format:

YYYY-MM-DD HH:MM:SS

so we massage it like this:

{{ (now() + timedelta(minutes = 30)).timestamp() | timestamp_local  }}

When used in a Time Trigger, it will trigger in 30 minutes. However, what if during those 30 minutes someone locks the door lock? Unlike a timer, you can’t cancel a Time Trigger.

Arguably it’s not a big deal if the Time Trigger is allowed to trigger even if the door is already locked. The automation’s action determines the lock is already locked so it skips locking it.

However, what if you’re a perfectionist and really want to prevent that Time Trigger from triggering? :thinking:

You can’t cancel it but you can set it to a time in the past which will effectively prevent it from triggering. That’s easily done by simply subtracting 30 minutes from the current time, as opposed to adding it.

We can either do something like this:

timedelta(minutes = 30 if trigger.to_state.state == 'locked' else -30)

or if we want to type the offset value just once not twice (because we don’t like duplicating constants or maybe we will make it an adjustable input_number in the future), we multiply it by either 1 or -1

timedelta(minutes = 30 * (1 if trigger.to_state.state == 'locked' else -1))

Putting it all together, we get this single, compact automation that relies on just two entities (lock.door and input_datetime.door_lock) and is able to survive a restart and Reload Automations.

alias: Automatic Door Lock
id: automatic_door_lock
mode: queued
trigger:
- platform: state
  entity_id: lock.door
- platform: time
  at: input_datetime.door_lock
action:
- choose:
  - conditions: "{{ trigger.platform == 'state' }}"
    sequence:
    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.door_lock
      data:
        datetime: >
          {{ (now() + timedelta(minutes = 30 * (1 if trigger.to_state.state == 'unlocked' else -1))).timestamp() | timestamp_local  }}
  - conditions:
    - "{{ trigger.platform == 'time' }}"
    - "{{ is_state('lock.door', 'unlocked') }}"
    sequence:
    - service: lock.lock
      target:
        entity_id: lock.door
4 Likes

Thanks!

I’ve been using that style of configuration (minus the nifty compact template) for any timed automations that are any more than just several minutes for a while now.

You just inspired me to re-check my config and there are few I haven’t switched over yet but most are non-critical.

How about adding a startup trigger to it, and checking to see if the input_datetime is in the very recent past (that is, the door should have been locked by now) when the door is unlocked?

That would then cover it being down, but restarting, when it should have locked the door.

(PS: What you’ve done is pretty sweet)

I think this will cover that:

alias: Automatic Door Lock
id: automatic_door_lock
mode: queued
trigger:
- platform: state
  entity_id: lock.door
- platform: time
  at: input_datetime.door_lock
- platform: homeassistant
  event: start
action:
- choose:
  - conditions: "{{ trigger.platform == 'state' }}"
    sequence:
    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.door_lock
      data:
        datetime: >
          {{ (now() + timedelta(minutes = 30 * 1 if trigger.to_state.state == 'unlocked' else -1)).timestamp() | timestamp_local  }}
  - conditions: 
    - "{{ trigger.platform == 'homeassistant' }}"
    - "{{ as_timestamp(now()) - as_timestamp(states('input_datetime.door_lock')) < 120 }}"
    - "{{ is_state('lock.door', 'unlocked') }}"
    sequence:
    - service: lock.lock
      target:
        entity_id: lock.door
  - conditions:
    - "{{ trigger.platform == 'time' }}"
    - "{{ is_state('lock.door', 'unlocked') }}"
    sequence:
    - service: lock.lock
      target:
        entity_id: lock.door
3 Likes

What would be really neat is if we could instantiate a datetime object just for use by the automation and is removed if the automation is deleted.

that way it would negate the need to define an input_datetime entity for every automation that is built like this.

I think you have an extra “sequence:” in there around line 28.

You’re right; that’s a scenario I didn’t consider, namely where Home Assistant is offline for longer than the offset value (30 minutes in this example) so when it starts the input_datetime is now in the past and the Time Trigger never fires.

What you’ve added constitutes a ‘grace period’ where if the input_datetime is less than 2 minutes old on startup, the lock gets locked (if its unlocked). This makes it more robust.

I can’t help but think there may be an edge-case where it locks the door on startup when it shouldn’t. However, I feel the opportunity for this to happen is minimized if the grace period is kept well short of the negative offset value (which it definitely is in this case: 30 minutes in the past versus 2 minutes).


EDIT

There’s a parallel here. When I created the restoration system for active timers, someone asked to have timers, that expired while Home Assistant was offline, to be restarted on startup just for a brief moment (a second or two) so that they could finish and send a timer.finished event (to trigger whatever depended on the timer’s completion). In effect, they asked for the same ‘grace period’ you suggested, namely to give it one more chance even though it had already expired (during the time when Home Assistant was offline).

1 Like

What you’ve proposed, one entity’s existence is dependent on another, is quite sophisticated and specialized. I would use it but it might be challenging to provide enough use-cases to convince someone to implement it. :slight_smile:

I believe we already kind of do something similar with script/action specific variables.

I would think it wouldn’t be too much work to extend that to something like this. (Not that I would know how to do it myself tho. but for someone who knows… :slightly_smiling_face:)

We could define automation-wide (instead of just action-wide) variables to be used at various points (trigger, condition, action) in that automation only.

One of the limitations in Home Assistant is that it can’t append a new or modified automation to its reportory of automations without restarting all of them. If it could do that then it would at least avoid trouncing all running triggers and actions after executing Reload Automations. It would also lay the groundwork for the ability to restart the whole menagerie on startup from exactly where it left off.

Fixed now - thanks for spotting that.

That’s what I get for just throwing something together in the browser :joy: :man_facepalming:

1 Like

Great stuff guys! This works well. One question though, if I remove the homeassistant start trigger, will it continue on with the input_datetime trigger? I’ve tested it so far by letting the input_datetime reach it’s set value and the door locks as it should. But once I restart HA, the input_datetime value changes to the time HA is booted up and the door locks right away. I still want it to go the full 30mins, then lock.

If you use the version I posted above, it doesn’t contain the homeassistant trigger and actions related to that trigger.

?

That is unusual behavior because the value of an input_datetime is not modified on startup and retains its original value (before the restart). To change an input_datetime’s value on startup would require an automation to do it. However, there’s nothing in either my version or Tinkerer’s that sets that value on startup. EDIT I think I figured out why it’s locking on startup and it’s due to a small but impactful error! It only manifests itself when used with the additional code Tinkerer provided (i.e. it serves to leverage my mistake).

Add parentheses to the two spots shown here:

      data:
        datetime: >
          {{ (now() + timedelta(minutes = 30 * 1 if trigger.to_state.state == 'unlocked' else -1)).timestamp() | timestamp_local  }}
                                              ^                                                 ^
                                              |                                                 |

so that it looks like this:

      data:
        datetime: >
          {{ (now() + timedelta(minutes = 30 * (1 if trigger.to_state.state == 'unlocked' else -1))).timestamp() | timestamp_local  }}

Without the parentheses, it uses 30 * 1 when the state is unlocked and -1 when the state is locked. That’s not how we want it to work!

Just tried. The input_datetime value now seems to go back in the past to 30mins and the door still locks on HA restart. Odd!

It should do that when the fan is turned off manually.

This is happening with the original version I posted? Post the automation you are currently using.

Only used @Tinkerer’s version. This is what the automation looks like:

alias: Security - Auto Lock Door
description: ''
trigger:
  - platform: state
    entity_id: lock.front_door_lock
  - platform: time
    at: input_datetime.auto_lock
  - platform: homeassistant
    event: start
condition: []
action:
  - choose:
      - conditions:
          - condition: template
            value_template: '{{ trigger.platform == ''state'' }}'
        sequence:
          - service: input_datetime.set_datetime
            target:
              entity_id: input_datetime.auto_lock
            data:
              datetime: >
                {{ (now() + timedelta(minutes = 30 * (1 if
                trigger.to_state.state == 'unlocked' else -1))).timestamp() |
                timestamp_local  }}
      - conditions:
          - condition: template
            value_template: '{{ trigger.platform == ''homeassistant'' }}'
          - condition: template
            value_template: >-
              {{ as_timestamp(now()) -
              as_timestamp(states('input_datetime.auto_lock')) < 120 }}
          - condition: template
            value_template: '{{ is_state(''lock.front_door_lock'', ''unlocked'') }}'
        sequence:
          - service: tts.google_cloud_say
            data:
              message: Locking the front door.
              entity_id: media_player.mercury
          - service: lock.lock
            target:
              entity_id: lock.front_door_lock
      - conditions:
          - condition: template
            value_template: '{{ trigger.platform == ''time'' }}'
          - condition: template
            value_template: '{{ is_state(''lock.front_door_lock'', ''unlocked'') }}'
        sequence:
          - service: tts.google_cloud_say
            data:
              message: Locking the front door.
              entity_id: media_player.mercury
          - service: lock.lock
            target:
              entity_id: lock.front_door_lock
mode: single