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.
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.
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.
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?
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
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
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).
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.
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⌠)
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
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 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).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.
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