Stopwatch with start/stop/resume, lap and reset

So there seems to be an issue, certainly in my case at least, of the input bool for reset not working correctly anymore.

If the stopwatch is at 00:00:00 and I toggle ON the input bool for start stopwatch, the tic tac starts running, and the stopwatch starts counting up in seconds, as expected. If I then toggle the start stopwatch again, it stops the stopwatch as expected. However, if i then toggle the input bool for reset, the time does NOT reset. Imstead, it shows what the counter ‘would’ have been on, if i had left it running. For example, if I stop the stopwatch on 00:00:05, wait 5 seconds, then toggle reset, the stopwatch changes to 10 seconds and doesn’t move. If I then toggle reset a second time, the stopwatch resets back to 00:00:00.

It’s as if the rest toggle has a ‘hidden state’ to it. Once I’ve successfully reset to 00:00:00, after having to toggle reset twice, as explained, if I then toggle reset, nothing happens visually, as expected however this seems to oit thr stopwatch back into a working state. Togling start starts it, togglign start again stops it, and toggling reset JUST ONCE desets back to 00:00:00. I have nonidea why this works, but i can reproduce it every time.

Can someone else tests this and see if they are experiencing the same?

Hi! For me the behaviour is essentially the same but the last thing that you mentioned does not happen for me or maybe I just misunderstood you.

If I toggle the stopwatch ON, is starts. When I stop it, it stops. When I reset it the first time, the time displayed is as if it was running (same as I described above and what you describe in your post). Then when I reset again, it goes back to 0.
However, when I then start it, stop it and reset it, it does not go to 0, I still need to hit reset for the second time while you are saying that you only need to reset it once?

Edit to add: When I start the stopwatch but instead of stopping it I hit reset, it stops the same way as if I chose to pause it. The thing is that when I then resume it and then reset it, the time goes down instead of up. So if I run it for 5 seconds, then reset it (without pausing) then resume and let it run to 10 seconds and then reset it again, it goes back to 5 seconds. I have no idea what that means, I just share the observation hoping that someone might know what it means.

Edit2: I have tested a bit more starting, pausing and resetting the timer. Now I can sometimes get to the state where starting, pausing and resetting once actually resets the stopwatch but I can then go back to the issue where starting, pausing and resetting once adds more time. I have a feeling that whether the resetting once or twice is needed depends on how long you let the stopwatch run and how long you wait with resetting it. Every time I reproduced the need to reset twice was when I let the timer run for for example 20 seconds and then waited 20 seconds while other tests where only 5 seconds. I do not know if the time I need to wait will keep on going up or not, just my initial observation.

Edit3: Another observation. For me, if I run the stopwatch for 0-5 seconds, pause it and then reset it, it goes to 0. If I run it for above 5 seconds, pause it and then reset it, it will go up by the time I waited between pausing and resetting.

Edit4: I take it back. I sometimes see a patter but then when I test the same thing 10 minutes later, the pattern is not there anymore. It feels like something is happening in the background that affects all this.

Hi again,

I am now adding a new comment because I think that now I have finally found something more meaningful. I created a simple entities card with the last change status to check what is happening when I start, stop, reset etc the stopwatch:
Screenshot 2024-10-28 120240

When playing with it, I discovered that when you reset the timer and the time is added, the TicTac gets reset as well or at least its status changes at the same time as the reset.
When I then wait a bit and hit reset for the second time, the TicTac’s status does not update and the stopwatch gets properly reset.

I also tested starting the stopwatch by toggling the TicTac and the times then become really messed up.

I do not know how not to make it too long so just bear with me :slight_smile: essentially if you start the timer and then reset it twice it shows 0. If you then wait, for example 1 minute and then toggle TicTac, it will switch the stopwatch to 1min. If you toggle TicTac again, the stopwatchwill show 2 min and then 3 min. Every TicTac toggle will add time to the timer and that time is exactly the time that passes since you last started the timer. Resetting does not change anything here. I mean you can reset the timer and start it and it will run but I mean that the TicTac is somehow linked to the time that elapsed since the last toggling of the stopwatch start.

I also tried disabling the TicTac entity. That solves the issue. The stopwatch can be started, paused and reset and it always works, the time is never added to the stopwatch. The problem with that is that the status of the stopwatch gets updated live only when the TicTac is running. That’s how the stopwatch is built from what I understood. So if you disable the TicTac, you will only see the elapsed time when you pause/stop the stopwatch so it is not a solution but I hope that this narrows down what the issue is.

One more interesting thing. The TicTac has 2 states, right? On my entity card it is visualised with the toggle icon and it can be on or off. Every time when you stop the timer when the TicTac is in the OFF position and reset the timer, it will reset the first time. Every time the stopwatch is paused in the TicTac’s ON position, the time will be added the first time you reset and it will go to 0 the second time you reset. Which I think is in line with what I wrote above. Because the first reset is like toggling the TicTac which adds the time that elapsed since the last stopwatch’s status change!

If you want to test it, let me know if you see the same thing.

Hello folks
I have also taken a close look at this and I think we are hitting a race around condition that is dependent on the order the triggers are processed by the template and the automations. It seems that by the time a template trigger is processed the input booleans may (or may not have changed). In addition when the ‘reset’ automation is processed it launches several triggers (start/stop and tactic) into the template which will be processed in due course (with no guarantee on the order). This is also compounded by the tic tac triggers that can arrive during the reset process. I added extra lap entries and observed up to two residual tactic triggers processed after the reset started (one in mid flight, one triggered by the reset automation).

I tried adding a wait on trigger for the reset process (triggered from the “00:00” status of the stopwatch sensor but it wasn’t quite there. I also considered a ‘resetting’ state to allow all the trigger to work through.

Below is my version of the code which takes account of the trigger type inside of the template as well as extra checks on sensor attributes. I looks like a bit of overkill but does seem to work. Ideally the template code would only use template variables and trigger details (to avoid scheduling race around issues), but did not succeed.

The code seems to work for me so try out and see if it works for you.

input_boolean:
  start_stopwatch:
    # It triggers stopwatch to start/stop(pause)
    name: Start/Stop Stopwatch
#    initial: off
  reset_stopwatch:
    # It triggers stopwatch to reset
    name: Reset
#    initial: off
  tictac_stopwatch:
    # Pendulum of the stopwatch
    name: TicTac
#    initial: off
  lap_stopwatch:
    # It triggers stopwatch to show lap time
    name: Lap
    icon: mdi:camera-outline
#    initial: off

template:
  - trigger:
    # Stopwatch sensor with Start, Stop/Pause, Reset and Lap features. Hundreds of second precission
    - platform: state
      entity_id: input_boolean.start_stopwatch
      id: start_stop_pause
      from:
        - 'on'
        - 'off'     
      to:
        - 'off' 
        - 'on'
      
    - platform: state
      entity_id: input_boolean.reset_stopwatch
      from: "off"
      to: "on"
      id: reset

    - platform: state
      entity_id: input_boolean.lap_stopwatch
      from: "off"
      to: "on"
      id: lap

    - platform: state
      entity_id: input_boolean.tictac_stopwatch
      id: tictac
      from:
        - 'on'
        - 'off'     
      to:
        - 'off' 
        - 'on'
    sensor:
      - name: "Stopwatch"
        state: >-
          {% if (trigger.id =='reset') %}
              {{ '00:00' }}
              {# RESET THE STOPWATCH SENSOR #}
          {% elif  (trigger.id =='start_stop_pause') and (trigger.to_state.state == 'off') and is_state('input_boolean.start_stopwatch','off') and is_state('input_boolean.lap_stopwatch','off') and is_state_attr('sensor.stopwatch','running','on') %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') %}
              {{ value|float|timestamp_custom("%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string }}
              {# STOP / PAUSE STOPWATCH #}          
          {% elif  (trigger.id =='tictac') and is_state('input_boolean.start_stopwatch','on') and is_state_attr('sensor.stopwatch','running','on') %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') %}
              {{ value|float|timestamp_custom("%M:%S", False) |string }}
              {# UPDATE DUE TO THE TIC TAC KEEPS DISPLAY MOVING #}
          {% else %}
              {{ states('sensor.stopwatch') }}
              {# IGNORE ALL OTHER TRIGGERS #}
          {% endif %}
        icon: mdi:timer
        attributes:
          initial_time: >-
            {% if (trigger.id =='reset') %}
              {{ 0 }}
            {% elif (trigger.id =='start_stop_pause') and (trigger.to_state.state == 'on') and is_state('input_boolean.start_stopwatch', 'on') and is_state_attr('sensor.stopwatch','running','off') %}
              {{ as_timestamp(now()) }}
              {# INITIAL TIME OF THE CURRENT RUNNING SESSION #}
            {% else %}
              {{ state_attr('sensor.stopwatch','initial_time') }}
              {# IGNORE ALL OTHER TRIGGERS #}
            {% endif %}
          elapsed_time: >-
            {% if (trigger.id =='reset') %}
              {{ 0 }}
              {# RESET ELAPSED TIME ATTRIBUTE #}
            {% elif (trigger.id =='start_stop_pause') and (trigger.to_state.state == 'off') and is_state('input_boolean.start_stopwatch','off') and is_state('input_boolean.lap_stopwatch','off') and is_state_attr('sensor.stopwatch','running','on')%}
              {{ as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') }}
              {# # STOPWATCH STOPPED/PAUSED - update the elapsed time (first run it is zero, if paused and restarted it added the last run period #}
               {# IGNORE ALL OTHER TRIGGERS #}
              {% else %}
            {{ state_attr('sensor.stopwatch','elapsed_time') }}
            {% endif %}
          running: >-
            {{ states('input_boolean.start_stopwatch') }}
          laps: >-
            {% if (trigger.id =='reset') %}
              {{[]}}
            {% elif (trigger.id =='lap') and is_state_attr('sensor.stopwatch','running','on') %}
              {% set data = namespace(laps=state_attr('sensor.stopwatch','laps')) %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') %}
              {% set data.laps = (data.laps + [value|float|timestamp_custom("%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string]) %}
              {{ data.laps }}
            {% else %}
              {{ state_attr('sensor.stopwatch','laps')}}
            {% endif %}  
  - trigger:
    # Numeric conversion of input_boolean.tictac_stopwatch to show as a gauge in Frontend
    - platform: state
      entity_id: input_boolean.tictac_stopwatch
    sensor:
      - unique_id: tictac_stopwatch
        name: >-
          {% if states('input_boolean.tictac_stopwatch') == 'on' %}
            Tic
          {% else %}
            Tac
          {% endif %}
        state: >-
          {% if is_state('input_boolean.tictac_stopwatch','on') %}
            {{ 1 }}
          {% else %}
            {{ 0 }}
          {% endif %}
        icon: >-
          {% if states('input_boolean.tictac_stopwatch') == 'off' %}
            mdi:clock-time-nine
          {% else %}
            mdi:clock-time-three-outline
          {% endif %}
  # Start/Stop(Pause) button
  - button:
    - unique_id: 'start_stop_stopwatch'
      name: Start/Stop
      icon: >-
        {% if states('input_boolean.start_stopwatch') == 'off' %}
          mdi:play-circle-outline
        {% else %}
          mdi:stop-circle-outline
        {% endif %}
      press:
        service: input_boolean.toggle
        target:
          entity_id: input_boolean.start_stopwatch

automation:
  - id: tic_tac_stopwatch
    alias: "Tic Tac Stopwatch"
    description: "It toggles input_boolean.tictac_stopwatch every second"
    trigger:
      - platform: time_pattern
        seconds: /1
    condition:
      - condition: state
        entity_id: input_boolean.start_stopwatch
        state: 'on'
    action:
      - service: input_boolean.toggle
        target:
          entity_id: input_boolean.tictac_stopwatch
    mode: single

  - id: reset_stopwatch
    alias: "Reset Stopwatch"
    description: "It reset input_booleans when input_boolean.reset_stopwatch is set to on"
    trigger:
      - platform: state
        entity_id: input_boolean.reset_stopwatch
        from: "off"
        to: "on"
    action:
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.start_stopwatch
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.tictac_stopwatch
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.reset_stopwatch
    mode: single

  - id: lap_stopwatch
    alias: "Lap Stopwatch"
    description: "It turns off input_boolean.lap_stopwatch when it is turned on"
    trigger:
      - platform: state
        entity_id: input_boolean.lap_stopwatch
        from: "off"
        to: "on"
    action:
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.lap_stopwatch
    mode: single```
1 Like

Thanks for writing up all of your findings! :slight_smile:

Yes, I can confirm that I’ve found the same as you, after further testing.

  1. If you stop the stopwatch while the tic tac boolean is in the off state (on ‘even’ number of seconds), and then toggle Reset, it resets the stopwatch counter to 00:00:00 as expected.

  2. If you stop the stopwatch while the tic tac is in the on state (on ‘odd’ number of seconds) however, and then toggle Reset, the stopwatch counter shows what the time would have been, if you had left it running. So if you let the stopwatch run for 00:00:09 seconds, then stop the stopwatch at 00:00:09 seconds, wait 5 more seconds, and then toggle Reset, the stopwatch counter changes to show 00:00:15 seconds. Then if you toggle Reset again, it clears back to 00:00:00.

Unless I used to always happen to stop the stopwatch on an even number, when the tic tac was in the off state, purely by coincidence, either manually or via automation, then something must have changed with a HA update which is causing this ‘new’ behaviour?

My use case for this stopwatch is for displaying how long my tumble dryer has been in each cycle during operation, but all i see is 00:00:00 due to this new behaviour, and the sensor.stopwatch never changes when my automations for state changes run. This used to work flawlessly for many months, so it seems something has changed and is causing this unexpected behaviour now :thinking:

Your point is valid. I have been using the stopwatch for a while and noticed it working for some HA releases and not for others. I was a real time programmer in my early career which pointed me towards the time phasing. I researched the ‘order’ that triggers are processed and found a comment to the effect that you cannot predict when templates vs automations are scheduled. Also there is a 1 second window of unpredictability caused by the tic tac timing you observed.

Give my script a try and see if it helps.

Just restarted HA and a new problem crept in with the reset while running - will carry on tracking it down…

Thanks for your testing and posting the code to try. I’ve just been testing your code, and initial tests look good for the start, then stop, then reset procedure. I think i see what you mean about the reset while running though, as it displays the current time, which I assume comes from the {{ as_timestamp(now()) }} code. The reset once stopped seems to work correctly in all the various tests I’ve done however, so that seems like good progress!

Update: I found that occasionally hitting reset while the stopwatch is running would yield an apparent random time to be set (due to stray triggers during the reset process). I have added the following refinement to the ‘running’ attribute that seems to have eliminated that problem.

          running: >-
            {% if (trigger.id =='reset') %}
              {{ "off" }}
            {% else %}
              {{ states('input_boolean.start_stopwatch') }}
            {% endif %}

The whole thing looks like this:

input_boolean:
  start_stopwatch:
    # It triggers stopwatch to start/stop(pause)
    name: Start/Stop Stopwatch
#    initial: off
  reset_stopwatch:
    # It triggers stopwatch to reset
    name: Reset
#    initial: off
  tictac_stopwatch:
    # Pendulum of the stopwatch
    name: TicTac
#    initial: off
  lap_stopwatch:
    # It triggers stopwatch to show lap time
    name: Lap
    icon: mdi:camera-outline
#    initial: off

template:
  - trigger:
    # Stopwatch sensor with Start, Stop/Pause, Reset and Lap features. Hundreds of second precision
    - platform: state
      entity_id: input_boolean.start_stopwatch
      id: start_stop_pause
      from:
        - 'on'
        - 'off'     
      to:
        - 'off' 
        - 'on'
      
    - platform: state
      entity_id: input_boolean.reset_stopwatch
      from: "off"
      to: "on"
      id: reset

    - platform: state
      entity_id: input_boolean.lap_stopwatch
      from: "off"
      to: "on"
      id: lap

    - platform: state
      entity_id: input_boolean.tictac_stopwatch
      id: tictac
      from:
        - 'on'
        - 'off'     
      to:
        - 'off' 
        - 'on'
    sensor:
      - name: "Stopwatch"
        state: >-
          {% if (trigger.id =='reset') %}
              {{ '00:00' }}
              {# RESET THE STOPWATCH SENSOR #}
          {% elif  (trigger.id =='start_stop_pause') and (trigger.to_state.state == 'off') and is_state('input_boolean.start_stopwatch','off') and is_state('input_boolean.lap_stopwatch','off') and is_state_attr('sensor.stopwatch','running','on') %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') %}
              {{ value|float|timestamp_custom("%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string }}
              {# STOP / PAUSE STOPWATCH #}          
          {% elif  (trigger.id =='tictac') and is_state('input_boolean.start_stopwatch','on') and is_state_attr('sensor.stopwatch','running','on')  %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') %}
              {{ value|float|timestamp_custom("%M:%S", False) |string }}
              {# UPDATE DUE TO THE TIC TAC KEEPS DISPLAY MOVING #}
          {% else %}
              {{ states('sensor.stopwatch') }}
              {# IGNORE ALL OTHER TRIGGERS #}
          {% endif %}
        icon: mdi:timer
        attributes:
          initial_time: >-
            {% if (trigger.id =='reset') %}
              {{ 0 }}
            {% elif (trigger.id =='start_stop_pause') and (trigger.to_state.state == 'on') and is_state('input_boolean.start_stopwatch', 'on') and is_state_attr('sensor.stopwatch','running','off') %}
              {{ as_timestamp(now()) }}
              {# INITIAL TIME OF THE CURRENT RUNNING SESSION #}
            {% else %}
              {{ state_attr('sensor.stopwatch','initial_time') }}
              {# IGNORE ALL OTHER TRIGGERS #}
            {% endif %}
          elapsed_time: >-
            {% if (trigger.id =='reset') %}
              {{ 0 }}
              {# RESET ELAPSED TIME ATTRIBUTE #}
            {% elif (trigger.id =='start_stop_pause') and (trigger.to_state.state == 'off') and is_state('input_boolean.start_stopwatch','off') and is_state('input_boolean.lap_stopwatch','off') and is_state_attr('sensor.stopwatch','running','on')%}
              {{ as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') }}
              {# # STOPWATCH STOPPED/PAUSED - update the elapsed time (first run it is zero, if paused and restarted it added the last run period #}
               {# IGNORE ALL OTHER TRIGGERS #}
              {% else %}
            {{ state_attr('sensor.stopwatch','elapsed_time') }}
            {% endif %}
          running: >-
            {% if (trigger.id =='reset') %}
              {{ "off" }}
            {% else %}
              {{ states('input_boolean.start_stopwatch') }}
            {% endif %}
          laps: >-
            {% if (trigger.id =='reset') %}
              {{[]}}
            {% elif (trigger.id =='lap') and is_state_attr('sensor.stopwatch','running','on') %}
              {% set data = namespace(laps=state_attr('sensor.stopwatch','laps')) %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') %}
              {% set data.laps = (data.laps + [value|float|timestamp_custom("%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string]) %}
              {{ data.laps }}
            {% else %}
              {{ state_attr('sensor.stopwatch','laps')}}
            {% endif %}  
  - trigger:
    # Numeric conversion of input_boolean.tictac_stopwatch to show as a gauge in Frontend
    - platform: state
      entity_id: input_boolean.tictac_stopwatch
    sensor:
      - unique_id: tictac_stopwatch
        name: >-
          {% if states('input_boolean.tictac_stopwatch') == 'on' %}
            Tic
          {% else %}
            Tac
          {% endif %}
        state: >-
          {% if is_state('input_boolean.tictac_stopwatch','on') %}
            {{ 1 }}
          {% else %}
            {{ 0 }}
          {% endif %}
        icon: >-
          {% if states('input_boolean.tictac_stopwatch') == 'off' %}
            mdi:clock-time-nine
          {% else %}
            mdi:clock-time-three-outline
          {% endif %}
  # Start/Stop(Pause) button
  - button:
    - unique_id: 'start_stop_stopwatch'
      name: Start/Stop
      icon: >-
        {% if states('input_boolean.start_stopwatch') == 'off' %}
          mdi:play-circle-outline
        {% else %}
          mdi:stop-circle-outline
        {% endif %}
      press:
        service: input_boolean.toggle
        target:
          entity_id: input_boolean.start_stopwatch

automation:
  - id: tic_tac_stopwatch
    alias: "Tic Tac Stopwatch"
    description: "It toggles input_boolean.tictac_stopwatch every second"
    trigger:
      - platform: time_pattern
        seconds: /1
    condition:
      - condition: state
        entity_id: input_boolean.start_stopwatch
        state: 'on'
    action:
      - service: input_boolean.toggle
        target:
          entity_id: input_boolean.tictac_stopwatch
    mode: single

  - id: reset_stopwatch
    alias: "Reset Stopwatch"
    description: "It reset input_booleans when input_boolean.reset_stopwatch is set to on"
    trigger:
      - platform: state
        entity_id: input_boolean.reset_stopwatch
        from: "off"
        to: "on"
    action:
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.start_stopwatch
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.tictac_stopwatch
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.reset_stopwatch
    mode: single


  - id: lap_stopwatch
    alias: "Lap Stopwatch"
    description: "It turns off input_boolean.lap_stopwatch when it is turned on"
    trigger:
      - platform: state
        entity_id: input_boolean.lap_stopwatch
        from: "off"
        to: "on"
    action:
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.lap_stopwatch
    mode: single

I will revisit the logic in the different condition statements to see if I can reduce the complexity.