Stopwatch with start/stop/resume, lap and reset

I’ve created a stopwatch with some nice features:

  • It allows to Start, Stop and Resume.
  • It allows to Reset.
  • It allows to save lap values while running.
  • It has hundredths of second precission.
  • It continues running with restarts of Home Assistant.
  • you don’t need to install any other package.
  • It can be controlled manually through a Frontend card, or automatically by triggering with input_boolean.start_stopwatch, input_boolean.reset_stopwatch and input_boolean.lap_stopwatch.

v1.0: Initial version.
v1.1: I’ve changed the code to show only hundreds of second when the stopwatch is stopped.
v1.2: trigger simplification.

Note: After creating the stopwatch if it has Unavailable or Unknown in its state, press the reset button, to reset it to “00:00:00”.

Here is the code (it can be copied to the package folder):

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
    - platform: state
      entity_id: input_boolean.reset_stopwatch
      from: "off"
      to: "on"
    - platform: state
      entity_id: input_boolean.lap_stopwatch
      from: "off"
      to: "on"
    - platform: state
      entity_id: input_boolean.tictac_stopwatch
    sensor:
      - name: "Stopwatch"
        state: >-
          {% if is_state('input_boolean.reset_stopwatch','on') %}
              {{ '00:00:00' }}
          {% elif  is_state('input_boolean.start_stopwatch','off') and is_state('input_boolean.lap_stopwatch','off') %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') %}
              {{ value|float|timestamp_custom("%H:%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string }}
          {% elif  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("%H:%M:%S", False) |string }}
          {% else %}
              {{ states('sensor.stopwatch') }}
          {% endif %}
        icon: mdi:timer
        attributes:
          initial_time: >-
            {% if is_state('input_boolean.start_stopwatch', 'on') and is_state_attr('sensor.stopwatch','running','off') %}
              {{ as_timestamp(now()) }}
            {% else %}
              {{ state_attr('sensor.stopwatch','initial_time') }}
            {% endif %}
          elapsed_time: >-
            {% if is_state('input_boolean.reset_stopwatch','on') %}
              {{ 0 }}
            {% elif is_state('input_boolean.start_stopwatch','off') and is_state('input_boolean.lap_stopwatch','off') %}
              {{ as_timestamp(now()) - state_attr('sensor.stopwatch','initial_time') + state_attr('sensor.stopwatch','elapsed_time') }}
            {% else %}
              {{ state_attr('sensor.stopwatch','elapsed_time') }}
            {% endif %}
          running: >-
            {{ states('input_boolean.start_stopwatch') }}
          laps: >-
            {% if is_state('input_boolean.reset_stopwatch','on') %}
              {{[]}}
            {% elif is_state('input_boolean.lap_stopwatch','on') 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("%H:%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’ve create two alternative Lovelace cards with animated seconds hand:

Peek 2023-01-19 11-50

type: vertical-stack
cards:
  - type: entity
    entity: sensor.stopwatch
  - type: horizontal-stack
    cards:
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: toggle
        entity: button.start_stop
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: toggle
        entity: input_boolean.lap_stopwatch
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: toggle
        entity: input_boolean.reset_stopwatch
  - type: horizontal-stack
    cards:
      - type: markdown
        content: |-
          **Laps:**
          {% for i in range(state_attr('sensor.stopwatch','laps')|length) %}
            {{ (i+1)|string + ': ' + state_attr('sensor.stopwatch','laps')[i]}}
          {% endfor %}
      - type: gauge
        entity: sensor.template_tictac_stopwatch
        min: 0
        max: 1
        needle: true

and this one:
Peek 2023-01-19 11-52

type: vertical-stack
cards:
  - type: horizontal-stack
    cards:
      - type: entity
        entity: sensor.stopwatch
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: none
        entity: sensor.template_tictac_stopwatch
        show_state: false
        icon_height: 40px
  - type: horizontal-stack
    cards:
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: toggle
        entity: button.start_stop
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: toggle
        entity: input_boolean.lap_stopwatch
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: toggle
        entity: input_boolean.reset_stopwatch
  - type: markdown
    content: |-
      **Laps:**
      {% for i in range(state_attr('sensor.stopwatch','laps')|length) %}
        {{ (i+1)|string + ': ' + state_attr('sensor.stopwatch','laps')[i]}}
      {% endfor %}

If you use Mushroom integration, (you can install through HACS) it can be used for the card instead of previuos ones. In that case, you won’t need the button section of the stopwatch code, so you can safely remove and create several stopwatches without problems.

Peek 2023-07-12 11-33

The card code with Mushrrom is:

type: vertical-stack
cards:
  - type: entity
    entity: sensor.stopwatch
  - type: horizontal-stack
    cards:
      - type: custom:mushroom-template-card
        primary: |-
          {% if is_state('input_boolean.start_stopwatch','off') %}
            {% if is_state('sensor.stopwatch','00:00:00') %}
              Start
            {% else %}
              Resume
            {% endif %}
          {% else %}
            Pause/Stop
          {% endif %}
        secondary: ''
        icon: |-
          {% if is_state('input_boolean.start_stopwatch','off') %}
            {% if is_state('sensor.stopwatch','00:00:00') %}
              mdi:play-circle-outline
            {% else %}
              mdi:pause-circle-outline
            {% endif %}
          {% else %}
            mdi:stop-circle-outline
          {% endif %}
        entity: input_boolean.start_stopwatch
        icon_color: |
          {% if is_state('input_boolean.start_stopwatch','off') %}
            {% if is_state('sensor.stopwatch','00:00:00') %}
              green
            {% else %}
              orange
            {% endif %}
          {% else %}
              red
          {% endif %}
        hold_action:
          action: none
        double_tap_action:
          action: none
      - type: custom:mushroom-template-card
        primary: Lap
        secondary: ''
        icon: mdi:camera-outline
        entity: input_boolean.lap_stopwatch
        icon_color: |-
          {% if is_state('input_boolean.start_stopwatch','off') %}
            grey
          {% else %}
            blue
          {% endif %}
        hold_action:
          action: none
        double_tap_action:
          action: none
      - type: custom:mushroom-template-card
        primary: Reset
        secondary: ''
        icon: mdi:close-circle-outline
        entity: input_boolean.reset_stopwatch
        icon_color: |-
          {% if is_state('input_boolean.start_stopwatch','off') %}
            {% if is_state('sensor.stopwatch','00:00:00') %}
              grey
            {% else %}
              red
            {% endif %}
          {% else %}
            red
          {% endif %}
        hold_action:
          action: none
        double_tap_action:
          action: none
  - type: horizontal-stack
    cards:
      - type: markdown
        content: |-
          **Laps:**
          {% for i in range(state_attr('sensor.stopwatch','laps')|length) %}
            {{ (i+1)|string + ': ' + state_attr('sensor.stopwatch','laps')[i]}}
          {% endfor %}
      - type: gauge
        entity: sensor.template_tictac_stopwatch
        min: 0
        max: 1
        needle: true

I hope anybody finds it useful.

Note: I recommend to remove all the entities created from recorder, as it’s useless to record them in database. To do that, add next lines to recorder in configuration.yaml:

recorder:
  ...
  exclude:
    entity_globs:
      - automation.*stopwatch
      - input_boolean.*stopwatch
      - sensor.*stopwatch
31 Likes

Thank you for posting this but I don’t see any use-case where this might be valuable or required.

When I need a timer / stopwatch I’ll use my smartphone. No need to start the HA-App first or to walk up to a HA-wall panel.

But that’s just my 2 cents, your mileage may vary.

I agree that timers are more useful, but from time to time there is a request in the HA community about a simple stopwatch.

You can see several use cases and requests in the community:

https://community.home-assistant.io/t/baby-sleep-stopwatch-timer/386170
https://community.home-assistant.io/t/garden-runners/188840
https://community.home-assistant.io/t/how-to-create-a-stopwatch-start-stop-via-wifi-button/121761
https://community.home-assistant.io/t/create-a-sensor-that-is-a-stopwatch-of-the-state-of-another-sensor/391066
https://community.home-assistant.io/t/stopwatch-helper/441079
https://community.home-assistant.io/t/creating-a-stopwatch-best-way-to-accomplish-this/348502
https://community.home-assistant.io/t/simple-stopwatch/306011
https://community.home-assistant.io/t/timer-and-stopwatch/188524

4 Likes

Some people will surely value your work then. :+1:
I did not mean to make you feel bad, I was just wondering…

2 Likes

Never mind. I appreciate your comment.

Just wanted to say thank you for this. Exactly what i needed. Need a stop watch to time how long a switch has been on (to calibrate a doser pump) and this was much easier than trying to use my phone and remembering to stop it and watch the switch.

2 Likes

Nice to hear it’s useful for you.

1 Like

Seems interesting, as I’m trying to bring back as many NodeRED automations as possible back inside HA in prevision of the new editor in 2022.9.0

As I’m counting the TV time, iPad time, Laptop time and other things, I’ll have to duplicate everything with a post/prefix I guess.

Moreover, it will be only editable if in automations.yaml file so I’ll try to adapt your code outside of the package folder.

2 Likes

You can create the helpers in HA → Settings → Helpers. The template sensors in configuration.yaml and the automation in Settings → Automations & Scenes → Automations.

Hope it will be useful for you.

2 Likes

a thousand thanks! may I ask you how can I create 6 more? what values ​​should i change? I tried to modify all the sensors but it gives me a “?” previous sensors

1 Like

You should create another file in packages folder for each stopwatch (stopwatch2.yaml) in which you copy the code and change the name of every input_boolean, sensor, button and automation.

For instance, for stopwach2 it could be this code:

input_boolean:
  start_stopwatch2:
    # It triggers stopwatch to start/stop(pause)
    name: Start/Stop Stopwatch2
#    initial: off
  reset_stopwatch2:
    # It triggers stopwatch to reset
    name: Reset2
#    initial: off
  tictac_stopwatch2:
    # Pendulum of the stopwatch
    name: TicTac2
#    initial: off
  lap_stopwatch2:
    # It triggers stopwatch to show lap time
    name: Lap2
    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_stopwatch2
      from: "off"
      to: "on"
    - platform: state
      entity_id: input_boolean.start_stopwatch2
      from: "on"
      to: "off"
    - platform: state
      entity_id: input_boolean.reset_stopwatch2
      from: "off"
      to: "on"
    - platform: state
      entity_id: input_boolean.lap_stopwatch2
      from: "off"
      to: "on"
    - platform: state
      entity_id: input_boolean.tictac_stopwatch2
    sensor:
      - name: "Stopwatch2"
        state: >-
          {% if is_state('input_boolean.reset_stopwatch2','on') %}
              {{ '00:00:00' }}
          {% elif  is_state_attr('sensor.stopwatch2','running','on') %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch2','initial_time') + state_attr('sensor.stopwatch2','elapsed_time') %}
              {{ value|float|timestamp_custom("%H:%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string }}
          {% else %}
              {{ states('sensor.stopwatch2') }}
          {% endif %}
        icon: mdi:timer
        attributes:
          initial_time: >-
            {% if is_state('input_boolean.start_stopwatch2', 'on') and is_state_attr('sensor.stopwatch2','running','off') %}
              {{ as_timestamp(now()) }}
            {% else %}
              {{ state_attr('sensor.stopwatch2','initial_time') }}
            {% endif %}
          elapsed_time: >-
            {% if is_state('input_boolean.reset_stopwatch2','on') %}
              {{ 0 }}
            {% elif is_state('input_boolean.start_stopwatch2','off') and is_state('input_boolean.lap_stopwatch2','off') %}
              {{ as_timestamp(now()) - state_attr('sensor.stopwatch2','initial_time') + state_attr('sensor.stopwatch2','elapsed_time') }}
            {% else %}
              {{ state_attr('sensor.stopwatch2','elapsed_time') }}
            {% endif %}
          running: >-
            {{ states('input_boolean.start_stopwatch2') }}
          laps: >-
            {% if is_state('input_boolean.reset_stopwatch2','on') %}
              {{[]}}
            {% elif is_state('input_boolean.lap_stopwatch2','on') and is_state_attr('sensor.stopwatch2','running','on') %}
              {% set data = namespace(laps=state_attr('sensor.stopwatch2','laps')) %}
              {% set value = as_timestamp(now()) - state_attr('sensor.stopwatch2','initial_time') + state_attr('sensor.stopwatch2','elapsed_time') %}
              {% set data.laps = (data.laps + [value|float|timestamp_custom("%H:%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string]) %}
              {{ data.laps }}
            {% else %}
              {{ state_attr('sensor.stopwatch2','laps')}}
            {% endif %}  
  - trigger:
    # Numeric conversion of input_boolean.tictac_stopwatch to show as a gauge in Frontend
    - platform: state
      entity_id: input_boolean.tictac_stopwatch2
    sensor:
      - unique_id: tictac_stopwatch2
        name: >-
          {% if states('input_boolean.tictac_stopwatch2') == 'on' %}
            Tic
          {% else %}
            Tac
          {% endif %}
        state: >-
          {% if is_state('input_boolean.tictac_stopwatch2','on') %}
            {{ 1 }}
          {% else %}
            {{ 0 }}
          {% endif %}
        icon: >-
          {% if states('input_boolean.tictac_stopwatch2') == 'off' %}
            mdi:clock-time-nine
          {% else %}
            mdi:clock-time-three-outline
          {% endif %}
  # Start/Stop(Pause) button
  - button:
    - unique_id: 'start_stop_stopwatch2'
      name: >-
        {% if is_state('input_boolean.start_stopwatch2','off') %}
          {% if is_state('sensor.stopwatch2','00:00:00') %}
            Start2
          {% else %}
            Resume2
          {% endif %}
        {% else %}
          Stop/Pause2
        {% endif %}
      icon: >-
        {% if states('input_boolean.start_stopwatch2') == 'off' %}
          mdi:play-circle-outline
        {% else %}
          mdi:stop-circle-outline
        {% endif %}
      press:
        service: input_boolean.toggle
        target:
          entity_id: input_boolean.start_stopwatch2

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

  - id: reset_stopwatch2
    alias: "Reset Stopwatch2"
    description: "It reset input_booleans when input_boolean.reset_stopwatch2 is set to on"
    trigger:
      - platform: state
        entity_id: input_boolean.reset_stopwatch2
        from: "off"
        to: "on"
    action:
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.start_stopwatch2
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.tictac_stopwatch2
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.reset_stopwatch2
    mode: single

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

When you create the frontend cards, you have to use the new entity names.

I recommend you not to use the animated gauge if you have too many stopwatches and your HA runs on a not high-performance system. Another option if you don’t mind the stopwatch frontend refreshes every N seconds instead of every second, you can change the triggering frequency of the `tic_tac_stopwatch" automation:

seconds: /5

This will affect only the frontend refresh rate, not the precission of the watch.

1 Like

I changed the names of the various sensors in the configuration file, maybe that’s the error, now I try to create a new file for how many chronometers I need. Unfortunately with History Stats I had a problem last month and it changed all my values, losing everything. I have a raspberry 4 but mine is quite light, with no cameras. I have to time how long the radiators remain on in the rooms with your stopwatch.

  • I can ask you one last thing: can I not show the decimals, that is, not 00: 00: 00: 00 but only 00:00:00?

You have to change

          {% elif  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("%H:%M:%S", False) + '.' + ((value|float*100)%100)|round(0)|string }}

in the state of the Stopwatch sensor with

          {% elif  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("%H:%M:%S", False) }}

Thanks a lot! Where is the package folder located? Do I just need to create a yaml file in /package/ ?

1 Like

Yes, exactly in /config/packages/ folder

1 Like

Unfortunately I don’t see any packages folder within config (just “custom_components”). Do I need to create one?

Look here how to create and configure the packages folder.

1 Like

sensor.stopwatch state shows as unavailable. Any help? Just copied and pasted to the packages folder with the file name stopwatch.yaml. Not sure what I could have done wrong.

I really don’t know … State of the sensor should be ‘00:00:00’ if it’s not running and whatever value if it’s running.

You can try to press the ‘Reset’ button. As you can see in the code, if you press that button, the state of the sensor is set to ‘00:00:00’.

1 Like

Reset worked super dumb on my part. Really appreciate the help! And thank you so much for making the stopwatch!!

1 Like