Automations with timer or delay

What’s the best practice to manage a not critical operation?
I’m undecided between
Solution 1:

pseudo code
automation A:
  action: start timer.my_timer

automation B:
  trigger: timer.my_timer started
  action: turn on light

automation C:
  trigger: timer.my_timer finished
  action: turn off light

and the much simpler to maintain solution 2:

pseudo code
automation A:
  action: turn on light
  delay
  action: turn off light

Solution 1 has the pro that different automations can trigger the timer, so automation B acts like a function, and the timer can be “HA restart” persistent (i.e the timer can finish after the restar). Moreover, if you need to make sure that you turn off light in case of restart or automation reload,
you can add the triggers to automation C and manage it.

  trigger:
  - platform: homeassistant
    event: shutdown
    id: HA restart
  - platform: event
    event_type: automation_reloaded
    id: Automation reload

Solution 2 is much simpler, but it’s not “HA restart” persistent, because delay doesn’t survive to the restart. You can still manage a “failover” creating a new automation to stop mission critical entities upon restart or automation reload.

What do you think about this? Am I missing something?

The only way to persist through a restart is to store the time the light needs to turn off:

automation A:
  action: turn on light and store off time in an input_datetime

automation B:
  trigger: at off time
  action: turn off light

The following is a basic example of a single automation to turn on a light on/off at specific times.

- alias: example
  trigger:
    - id: 'on'
      platform: time
      at: '18:00:00'
    - id: 'off'
      platform: time
      at: '22:00:00'
  condition: []
  action:
    - service: 'light.turn_{{ trigger.id }}'
      target:
        entity_id: light.whatever

It is unaffected by a restart, or Reload Automations, unless it occurs precisely at the scheduled time. However, there’s a simple technique to mitigate that situation as well (involving a condition).

For more information, refer to the following post:

There’s also a technique to dynamically set the scheduled time so that it can behave like a timer (i.e. start time is not fixed and stop time is calculated based on the floating start time).

Those all seem to apply to fixed times (based on the sun) rather than a fixed delay though.

@tom_l Isn’t the restore option for a timer covering the use case? If you start a timer and after that you restart HA, if HA restarts after the planned finish time, it fires a timer.finished. Otherwise it will reactivate the timer.

So, if you have a not critical need, like a light, you can use a timer.
But if you are managing something critical (eg. you are running a pump to add bleach to your pool), you cannot rely on a timer restore.

Honestly I haven’t looked at timers for a while. Do they restore after a restart now?

For a non critical application (i.e. you don’t mind if the off automation is missed) the easiest way would just be a delay in the automation. That definitely will be interrupted by a restart but if it is not critical that does not matter :man_shrugging:

It’s relatively easy to store a fixed or variable time offset to be used in another time trigger, e.g

The off trigger is simple:

trigger:
  platform: time
  at: input_datetime.lounge_dehumidifier_stop_time

To set this to a fixed delay from now:

action:
  - service: input_datetime.set_datetime
    data:
      datetime: "{{ now() + timedelta(hours=4) }}" # 4 hours from now
    target:
      entity_id: input_datetime.lounge_dehumidifier_stop_time # the entity used in the off time trigger

Of if you want to set the delay from a dashboard, use an input_number to hold the delay:

  - service: input_datetime.set_datetime
    data:
      datetime: "{{ now() + timedelta(hours=states('input_number.lounge_dehumidifier_run_time')|int(1)) }}"
    target:
      entity_id: input_datetime.lounge_dehumidifier_stop_time

Yes, but they’re meant to serve as examples of the technique where delays and timers are replaced by methods that aren’t affected by restarting/reloading. FWIW, all of my time-based automation use the described technique and are unaffected by restarts/reloads. The calculated stop time I had mentioned is essentially what you demonstrated in your post.

If rzulian can provide a concrete example of an application (as opposed to pseudo-code), I can demonstrate how to adapt it to use the technique I described.

Yes (optionally); it was added recently although I haven’t experimented with it yet (because I converted most of my automations away from using timers and the few I still have are restored using the scripts I developed a long time ago). Eventually I’ll get around to it.

I guess the upshot is if the application is truly non-critical then it means it doesn’t matter if it uses a delay that is not given the opportunity to complete. For my purposes, anything that shouldn’t be left indefinitely in the wrong state is “critical” and doesn’t employ a delay.

1 Like

Below a critical automation that is executed by another automation.

- alias: bleach_inject
    id: "1559149426435"
    initial_state: true
    trigger: []
    condition: []
    action:
      - entity_id: switch.pompa_orp
        service: switch.turn_on
      - data_template:
          message: "{{now().strftime('%H:%M')}}[{{now().day}}/{{now().month}}]"
          title: "Bleach injection: ({{states('input_number.bleach_inject')}} ml)!!!"
        service: notify.SERVIZIO
      - delay: "{{ [0 , 60 * states('input_number.bleach_inject')|float/ states('input_number.bleach_speed')|float ]|max|int }}"
      - entity_id: switch.pompa_orp
        service: switch.turn_off
      - service: input_number.set_value
        data_template:
          entity_id: input_number.bleach_inject
          value: 0
      - data_template:
          message: "{{now().strftime('%H:%M')}}[{{now().day}}/{{now().month}}]"
          title: "Bleach injection end!!!"
        service: notify.SERVIZIO

@tom_l Thank you for your examples!

Ok, for a start that is not how you are supposed to use automations (manually triggering them from other automations). That should be a script (a script is an automation without a trigger) that your other automation can call.

The way you are doing it will work, it’s just not best practice.

So ignoring the delay issue for the moment. You would write it like this:

script:
  bleach_inject:
    sequence:
      - entity_id: switch.pompa_orp
        service: switch.turn_on
      - data_template:
          message: "{{now().strftime('%H:%M')}}[{{now().day}}/{{now().month}}]"
          title: "Bleach injection: ({{states('input_number.bleach_inject')}} ml)!!!"
        service: notify.SERVIZIO
      - delay: "{{ [0 , 60 * states('input_number.bleach_inject')|float/ states('input_number.bleach_speed')|float ]|max|int }}"
      - entity_id: switch.pompa_orp
        service: switch.turn_off
      - service: input_number.set_value
        data_template:
          entity_id: input_number.bleach_inject
          value: 0
      - data_template:
          message: "{{now().strftime('%H:%M')}}[{{now().day}}/{{now().month}}]"
          title: "Bleach injection end!!!"
        service: notify.SERVIZIO

And you would call it like this:

action:
  - service: script.bleach_inject

Or like this:

action:
  - service: script.turn_on
    target:
      entity_id: script.bleach_inject

The first example waits for the script to complete before moving to the next action. The second version runs the script in parallel with the next action, see https://www.home-assistant.io/integrations/script/#waiting-for-script-to-complete

For when you do it works great! I have a few timers I was using in places and my own custom restore state automation that handles all the things that I wish had restore capability (timers, trigger template entities, custom device trackers and alerts). After that update I removed timers from the list and used the native restore then fully tested it and found no issues.

Of note it is a little different then your logic (assuming that’s still accurate for your setup). If the timer finished while HA was shut down then it fires the timer.finished event on startup anyway.

As explained by tom_l, that’s not really an automation (it has no trigger). Post the other automation.

I’ve added the trigger that is starting the automation

  - alias: Piscina New Measurement Update
    trigger:
      platform: event
      event_type: ifttt_webhook_received
      event_data:
        action: pool_new_measurement
    condition: []
    action:
      - entity_id: switch.pompa_orp
        service: switch.turn_on
      - data_template:
          message: "{{now().strftime('%H:%M')}}[{{now().day}}/{{now().month}}]"
          title: "Bleach injection: ({{states('input_number.bleach_inject')}} ml)!!!"
        service: notify.SERVIZIO
      - delay: "{{ [0 , 60 * states('input_number.bleach_inject')|float/ states('input_number.bleach_speed')|float ]|max|int }}"
      - entity_id: switch.pompa_orp
        service: switch.turn_off
      - service: input_number.set_value
        data_template:
          entity_id: input_number.bleach_inject
          value: 0
      - data_template:
          message: "{{now().strftime('%H:%M')}}[{{now().day}}/{{now().month}}]"
          title: "Bleach injection end!!!"
        service: notify.SERVIZIO

Your modified automation doesn’t specify a mode so that means it uses the default and runs in single mode. Is that correct or does it actually have mode set to something else like restart or queued?

It’s correct, it runs in single mode

The reason I prefer timers for longer delays is:

  1. If you set the flag it restores the state on reboot,
  2. You can see running timers count down if you want to. I have a lovelace card showing all running timers somewhere.
  3. You can restart timers, cancel them, etc. (You can cancel a delay by disabling the script, but that is awkward)
  4. I think it is simpeler than scripting time logic.
2 Likes

Given that you are a proponent of timers, perhaps you can help rzulian modify the automation to employ a timer.

To recap.
Solution 1
using a timer with restore: true

alias: automation_using_timer
  trigger: your_trigger
  condition: []
  action:
   - entity_id: my_entity
     service: switch.turn_on
   - service: timer.start
     data:
      duration: "{{ 60 * states('input_number.minutes')|int }}"
     target:
      entity_id: timer.my_timer
  initial_state: true
  mode: single

alias: my_timer_finished
  trigger:
    - platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.my_timer
  condition: []
  action:
    - service: switch.turn_off
      target:
        entity_id: my_entity
  mode: single

Solution 2
using an automation with a delay and , if needed for critical entities, an automation to manage the HA restart and automation reload.

alias: automation_using_delay
  trigger: your_trigger
  condition: []
  action:
   - entity_id: my_entity
     service: switch.turn_on
   - delay: "{{ 60 * states('input_number.minutes')|int }}"
   - entity_id: my_entity
     service: switch.turn_off
  initial_state: true
  mode: single

alias: automation_for_critical_entities
  trigger:
  - platform: homeassistant
    event: shutdown
    id: HA restart
  - platform: event
    event_type: automation_reloaded
    id: Automation reload
  condition: []
  action:
   - entity_id: my_entity
     service: switch.turn_off
  initial_state: true
  mode: single

Solution 3
similar to solution 1 but setting a input_datetime to store when the second automation have to trigger.
I’m not sure what happens if a restart or reload occurs during the my_stop_time. Does the trigger run after the restart?

alias: automation_using_input_datetime
  trigger: your_trigger
  condition: []
  action:
   - entity_id: my_entity
     service: switch.turn_on
   - service: input_datetime.set_datetime
     data:
       datetime: "{{ now() + timedelta(minutes= states('input_number.minutes')) }}" 
     target:
       entity_id: input_datetime.my_stop_time 
  initial_state: true
  mode: single

alias: my_stop_time
  trigger:
    platform: time
    at: input_datetime.my_stop_time
  condition: []
  action:
    - service: switch.turn_off
      target:
        entity_id: my_entity
  mode: single
1 Like

Solution 1 - This looks good. Should work and be restart/reload proof

Solution 2 - I mean it works but your switch may shut off early. If you restart HA it’ll immediately shut off your switch. So if input_number.minutes is set to 120 and it turned off 30 seconds after you turned it on because you happened to restart HA I’d consider that a bug.

Solution 3 - This is a slightly less resilient version of the timer. For the most part it works just as well unless HA happens to be restarting or automations reloading at exactly the time set to input_datetime.my_stop_time. Which admittedly is pretty unlikely, its not a huge flaw. But there’s really no advantage to it over Solution 1: both require two automations, both require an extra helper. The only difference is Solution 1 is completely restart/reload proof whereas this one is 99% restart/reload proof. Therefore I don’t know why someone would pick this over Solution 1.

Another option for the turn off automation is to use a time pattern trigger (/5, /10, /15 minutes as desired), and then a state condition (not a trigger) to check and see if the pump has been on for more than 30(or whatever) minutes.

It removes the need for any helpers. It won’t be as precise, and may not fit if you want xx minutes and not approximately xx minutes.

The pump may run for a full xx minutes after a restart, but will shut off and not run all day.

It may not be appropriate for this use case if there are non-automated times the pump should be running more than xx minutes at a time.

The following single automation:

  1. Turns on the switch upon receiving a specific event
  2. Turns off the switch at the time specified by input_datetime.pompa_orp
  3. The switch’s on duration is controlled by input_number.pompa_orp
  4. After a restart it sets the switch to the appropriate state
alias: Piscina New Measurement Update
trigger:
- platform: event
  event_type: ifttt_webhook_received
  event_data:
    action: pool_new_measurement
- platform: time
  at: input_datetime.pompa_orp
- platform: homeassistant
  event: start
condition: []
action:
- variables:
    switch: switch.pompa_orp
- choose:
  - conditions: "{{ trigger.platform in ['event', 'time'] }}"
    sequence:
    - service: "switch.turn_{{ iif(trigger.platform == 'event', 'on', 'off') }}"
      target:
        entity_id: '{{ switch }}'
    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.pompa_orp
      data:
        timestamp: "{{ (now() + timedelta(minutes = states('input_number.pompa_orp')|int(0) * iif(trigger.platform == 'event', 1, -1))).timestamp() }}"
  - conditions: "{{ trigger.platform == 'homeassistant' }}"
    sequence:
    - service: "switch.turn_{{ 'on' if now() < states('input_datetime.pompa_orp') | as_datetime else 'off' }}"
      target:
        entity_id: '{{ switch }}'
  default: []

It’s based on the same principle I have used, for a over a year, for all of my scheduled automations to ensure they aren’t affected by restarts. A Time Trigger is controlled by an input_datetime that is dynamically set to a time in the future representing when the device should be turned off. The automation is also triggered by a restart and then determines if the device should be turned on or off.


EDIT

Correct typos.

1 Like