[DEPRECATED] ~~How to make active timers survive a restart~~


NOTE

This post is now deprecated and I am no longer supporting its code.

It is replaced by a new approach that doesn’t use a python_script and employs script enhancements that were introduced since this post was first created in November 2019.

Please refer to the new version presented here:




Deprecated post.

BugFix 1:

2020-06-07
The first automation failed to restore timers whose duration exceeded 24 hours. Durations of 24 hours or more appear in this format: 2 days, 6:30:00 as opposed to 54:30:00. The automation’s template has been enhanced and now properly handles durations expressed in days.


Introduction

Active timers don’t survive a restart of Home Assistant. On startup, previously active timers are set to idle and so they don’t resume their (interrupted) countdown.

Here’s a way to automatically resume active timers upon startup. It is suitable for timers whose duration is several minutes or longer (i.e. duration is longer than the time it takes your Home Assistant instance to restart). It consists of:

  • One input_text to store the state of all active timers.
  • Two automations. One stores the state of active timers. The other resumes them at startup.
  • One python_script to perform the actual resuming of active timers.

Step 1 - Input_Text

Create this input_text in your configuration.yaml file:

input_text:
  active_timers:
    name: Active Timers

Step 2 - Automations

Add the following two automations to wherever you keep them (configuration.yaml or automations.yaml).

The first automation is executed whenever a timer changes state and saves the states of all active timers. This automation requires customization:

  • Replace the trigger’s entities with the names of your own timers.
- alias: 'Active Timers Save'
  trigger:
    platform: state
    entity_id:
    - timer.timer1
    - timer.timer2
    - timer.timer3
  action:
  - service: input_text.set_value
    data_template:
      entity_id: input_text.active_timers
      value: >
        {% set ns = namespace(timers = '') %}
        {% for t in states.timer | selectattr('state', 'eq', 'active') | list  %}
          {% set d = t.attributes.duration.split(':') %}
          {% if 'day' in d[0] %}
            {% set z = d[0].split(', ') %}
            {% set h = ((z[0].split()[0] | int) * 24 + (z[1].split(':')[0] | int)) * 3600 %}
          {% else %}
            {% set h = d[0] | int * 3600 %}
          {% endif %}
          {% set s = h + (d[1]|int*60) + (d[2]|int) %}
          {% set c = ',' if not loop.first else '' %}
          {% set ns.timers = ns.timers ~ '{}{} {}'.format(c, t.object_id, t.last_changed.timestamp()|int + s) %}
        {% endfor %}
        {{ ns.timers }}

NOTE:
The first automation assumes you wish to save/resume all your timers. If you want it to be selective, let me know and I’ll explain how to modify it. Please be aware that simply excluding a timer from the trigger’s entity list is insufficient (because the action’s template is designed to get all active timers, whether you list them in the trigger or not).

The second automation is triggered at startup and calls the python_script to resume all (previously) active timers.

- alias: 'Active Timers Resume'
  trigger:
    platform: homeassistant
    event: start
  action:
    service: python_script.active_timers_resume

Step 3 - Python_Script

If you currently do not use the python_script integration, you will have to install it first (it’s easy). Full instructions can be found in its documentation. Here are the basic steps:

  1. Add python_script: to your configuration.yaml file.
  2. Create the following sub-directory: config/python_scripts
  3. Restart Home Assistant.

Create a new file in config/python_scripts called active_timers_resume.py with the following contents:

ts = hass.states.get('input_text.active_timers')
if ts is not None and ts.state != 'unknown' and ts.state != '':
  timers = ts.state.split(',')
  for t in timers:
    z = t.split()
    duration = int(z[1]) - int(datetime.datetime.now().timestamp())
    if duration > 0:
      service_data = {'entity_id': 'timer.{}'.format(z[0]), 'duration':'{}'.format(duration)}
      hass.services.call('timer', 'start', service_data, False)

Step 4 - Testing

Restart Home Assistant and perform a test:

  1. Start a timer with a duration of 2 minutes (more if it takes a long time for your instance of Home Assistant to restart).
  2. After you’ve confirmed the timer is running, restart Home Assistant.
  3. After it restarts, check the timer in the States page. It should be active and have a shorter duration.

FWIW, I’ve performed the test a few times and confirmed all active timers are resumed (with shorter durations, of course).

NOTE:
There is one caveat when using this save/resume system. After a restart, active timers are resumed with a new, shorter duration, representing the balance of their original duration. So if the timer’s duration was originally 5 minutes and it was interrupted at the 4 minute mark, it will resume at 4 minutes (minus whatever time it took for Home Assistant to restart). In other words, its new default duration is now 4 minutes. So the next time you start this timer, you will have to set its duration back to 5 minutes otherwise it will use 4 minutes.

15 Likes

hi ,
can you explain more how to combine it with my current automation ?

I’m not sure I understand what you mean by “combine it with my current automation”.

If you follow the instructions above, it will work for any timers you already have or that you add in the future. It doesn’t require you to modify your existing timers or automations.

Thank you very much for this. extremely useful.
Please advise on how to exclude some timers.

Would this do it:
Replace

{% for t in states.timer | selectattr('state', 'eq', 'active') | list %}

with

{% for t in states.timer | selectattr('state', 'eq', 'active')|selectattr('entity_id','ne','timer.cs7_washer')|selectattr('entity_id','ne','timer.cs7_laundry') | list %}

assuming i want to skip those 2 timers.

hi,
sorry about the questions but below you can see my automation:

  • id: Hot_Water
    alias: Hot_Water
    trigger:
    • entity_id: automation.hot_water
      for: ‘1’
      from: ‘off’
      platform: state
      to: ‘on’
      action:
    • entity_id: switch.sonoff_XXXX
      service: switch.turn_on
    • delay: 00:30:00
    • entity_id: switch.sonoff_XXXX
      service: switch.turn_off
    • entity_id: automation.hot_water
      service: automation.turn_off
    • data: {}
      entity_id: automation.hot_water
      service: automation.turn_off
      =============================================
      what do you meen about step two to define your own timer id ?

It means that in the sample automation named alias: ‘Active Timers Save’
you have to insert the timers associated with your particular automation

so what it is in my case ?
my automation turn sonoff for 30 min.
( you can ser above what i copied. )

Yes, but you use a delay, not a timer. Same effect, but different approach.

o.k
so can you show me what to change in my automation for using it with timer ?

hi,
i think its a little more complicated.
is there other way to acomplished it ?

EDIT

Overlooked to updated both examples with BugFix1.

@allanbell3d Thank you for bringing the oversight to my attention!


Here is how I would handle the situation where I wish to either exclude, or include, a subset of all timers. The choice of which to use depends on how many timers are involved.

Exclusion

Use this version when you only want to EXCLUDE a few timers. For example, you have 10 timers and want all but 3 of them to survive a restart.

  • In the trigger, specify the 7 timers to be preserved.
  • In the template’s timers variable, specify the 3 timers to be excluded.
- alias: 'Active Timers Save'
  trigger:
    platform: state
    entity_id:
    - timer.timer1
    - timer.timer2
    - timer.timer3
    - timer.timer4
    - timer.timer5
    - timer.timer6
    - timer.timer7
  action:
  - service: input_text.set_value
    data_template:
      entity_id: input_text.active_timers
      value: >
        {% set timers = states.timer
                        | rejectattr('object_id', 'in', ['timer8', 'timer9', 'timer10'])
                        | selectattr('state', 'eq', 'active')
                        | list %}
        {% set ns = namespace(timers = '') %}
        {% for t in timers %}
          {% set d = t.attributes.duration.split(':') %}
          {% if 'day' in d[0] %}
            {% set z = d[0].split(', ') %}
            {% set h = ((z[0].split()[0] | int) * 24 + (z[1].split(':')[0] | int)) * 3600 %}
          {% else %}
            {% set h = d[0] | int * 3600 %}
          {% endif %}
          {% set s = h + (d[1]|int*60) + (d[2]|int) %}
          {% set c = ',' if not loop.first else '' %}
          {% set ns.timers = ns.timers ~ '{}{} {}'.format(c, t.object_id, t.last_changed.timestamp()|int + s) %}
        {% endfor %}
        {{ ns.timers }}

Inclusion

Use this version when you only want to INCLUDE a few timers. For example, you have 10 timers but only want 3 of them to survive a restart.

  • In the trigger, specify the 3 timers to be preserved.
  • In the template’s timers variable, specify the same 3 timers.
- alias: 'Active Timers Save'
  trigger:
    platform: state
    entity_id:
    - timer.timer1
    - timer.timer2
    - timer.timer3
  action:
  - service: input_text.set_value
    data_template:
      entity_id: input_text.active_timers
      value: >
        {% set timers = [states.timer.timer1, states.timer.timer2, states.timer.timer3] 
                        | selectattr('state', 'eq', 'active')
                        | list %}
        {% set ns = namespace(timers = '') %}
        {% for t in timers %}
          {% set d = t.attributes.duration.split(':') %}
          {% if 'day' in d[0] %}
            {% set z = d[0].split(', ') %}
            {% set h = ((z[0].split()[0] | int) * 24 + (z[1].split(':')[0] | int)) * 3600 %}
          {% else %}
            {% set h = d[0] | int * 3600 %}
          {% endif %}
          {% set s = h + (d[1]|int*60) + (d[2]|int) %}
          {% set c = ',' if not loop.first else '' %}
          {% set ns.timers = ns.timers ~ '{}{} {}'.format(c, t.object_id, t.last_changed.timestamp()|int + s) %}
        {% endfor %}
        {{ ns.timers }}


So what’s different between these examples and the original one I posted?

The main difference is how the template refers to your timers:

  • In the original example, it simply uses states.timers which represents all of your timers.
  • In the exclusion example, it also uses states.timer but then rejects all the entities you specified in the list: rejectattr('object_id', 'in', ['timer8', 'timer9', 'timer10'])
  • In the inclusion example, it doesn’t use states.timer. It uses the timers you specify in the list: [states.timer.timer1, states.timer.timer2, states.timer.timer3] .
1 Like

Once again thank you very much for the guide and explanation.

As explained by francisp, your automation uses a delay not a timer. I’ve formatted your automation to make it easier to read:

- id: Hot_Water
  alias: 'Hot_Water'
  trigger:
    platform: state
    entity_id: automation.hot_water
    from: 'off'
    to: 'on'
    for: '1'
  action:
    - service: switch.turn_on
      entity_id: switch.sonoff_XXXX
    - delay: '00:30:00'
    - service: switch.turn_off
      entity_id: switch.sonoff_XXXX
    - service: automation.turn_off
      entity_id: automation.hot_water
    - service: automation.turn_off
      entity_id: automation.hot_water

At the very end of the action, it turns off automation.hot_water twice. I don’t understand why the automation is turning itself off and why it attempts to do that twice. However that’s a separate discussion.

If I understand you correctly, you are asking for assistance to convert your automation from using delay to using a timer. If that’s the case then please start a new, separate topic. This one is dedicated to discussing the preservation of timers after a restart.

If you are looking for a replacement for delay but believe timer is too complicated then you don’t have any other options. Those are the two principle ways of waiting for fixed period of time. There is also wait_template but it is designed to wait for a specified condition to become true.

thanks for your help.

I’ve just stumbled upon this post - thanks for sharing, the idea is interesting.

I restore timers individually just because a) there are not many long-duration timers in my system and b) I can control what timers to restore (as some of them just shouldn’t be).

Just want to point out that as you may know, input_text as any state object can hold no more than 255 characters as its state so I presume that if someone creative calls their timers like morning_routine_when_I_am_in_the_mood_to_excersise_for_longer and they have say, 3 or more of them, they’ll be in trouble because some timers’ data won’t make it to the input_boolean or (more likely) HA will refuse to accept a new one and they’ll end up with an old dataset (or empty string).

1 Like

Yes, you’re right. That’s a limitation of this design. At the time I was developing it, someone else was submitting a PR to make the restoration of active timers a native capability. Therefore, I considered my solution a stopgap measure until the PR was released. However, the PR was never released.

I can think of alternatives to using an input_text but they’re more complicated. If you know of a simple substitute, I’d like to hear about it. My impression is very few people have adopted what I’ve shared (and none have reported encountering the limitation) so I’m in no hurry to alter the design, especially if it makes it more complicated.

I see.
Just wanted to highlight the limitation for potential users.
Alternative can be using separate input_texts for each timer. It’s a bit more complicated and requires some naming agreements (and it’s just a concept, I haven’t implemented it).

maybe because doing it in bulk is rarely needed?

I think the reason is more comprehensive: I believe few people are even aware that active timers (and delays) aren’t restored. They discover it empirically, when their long duration timer fails to run to completion because it was interrupted by a restart.

1 Like

it’s says about it in the docs. so maybe it’s even more comprehensive - few people need to restore their timers at all as their timers never run for more than few minutes anyway.
just a guess, nevermind