Timer with Willow Voice / Named / Persistent

With inspiration from @DonNL’s work here. Thanks so much for your work on this. It has been amazing!
https://community.home-assistant.io/t/set-a-timer-using-ha-assist/

A lot of me rambling here. I went over the 32k characters limit for a post. So, all TLDR; words here. The real stuff comes in post #2.

I have written an automation that will start, stop, cancel, reset, pause, reset, and list running counters using voice and Willow for HA.

This has minimal changes to add more counters. It does require a couple of helpers for each timer you want to run (timer.* and input_text.*).

Other than the helpers, this is done all in one automation. Really, I am not sure if this is better or worse that other setups. But I really like it.

I’ll do my best to describe everything needed and give links as necessary. I am sure I missed something and certainly am open to fixing all issues.

Of course, we all know about the Year of the Voice
2023: Home Assistant’s year of Voice - Home Assistant (home-assistant.io)

So, I started with home automation software years ago using MisterHouse, X10 devices, and 3com Audreys.
MisterHouse (sourceforge.net) (last updated in 2017)
X10 (industry standard) - Wikipedia
3Com Audrey - Wikipedia

We were doing voice back then. Not sure if it was ‘web’ based or not. But, it didn’t seem to matter back then like it certainly does now.

So, seeing ‘local’ voice in a major automation project was very cool to me. I would not allow voice devices in the house. Our kid had some camera in his house and their browser adds certainly started showing items based on discussions in their house. I tested it with some very specific statements.

I bought five Atom M5s based on the announcement. I was less than impressed.

Once I saw the ESP32-S3-BOX-3 announced, I was impressed, but cautious. Then, I found the Willow project.
Home - Willow (heywillow.io)

This grabbed my attention! So, here we are.

1 Like

Yeah, here we go!

So, this automation will do as many voice timers as you have defined with minimal changes.

First, each timer requires two helpers. timer,* and input_text.* One of each for the timers you want available.

I currently have three of them. timer.auto* (1 through 3) and input_text.auto* (1 through 3). Need a fourth timer, add timer.auto4 and input_text.auto4. No problem.

Before I get to the good stuff, here is a card for the timers (uses auto-entities and timer-bar-card)

type: custom:auto-entities
card:
  type: entities
filter:
  template: >
    {%- for timer in states.timer | selectattr('entity_id', 'match',
    'timer.auto*') | map(attribute='entity_id') | list -%}
          {{
            {"type": "custom:timer-bar-card","entity": timer, "name": states('input_text.' ~ timer.split('.')[1]).split('|')[0][0] | upper ~ states('input_text.' ~ timer.split('.')[1]).split('|')[0][1:] | lower }
          }},
    {%- endfor -%}

also for testing (uses auto-entities too)

type: custom:auto-entities
card:
  type: entities
filter:
  include:
    - entity_id: input_text.auto*

So, create helpers as follows:
timer.auto* (1-whatever) set it to restore so the timers will carry over between reboots.
input_text.auto* (1-whatever)

Two rest commands replacing your WAS server IP/Name:

  willow_notify:
    url: http://[your was server]:8502/api/client?action=notify
    method: POST
    content_type: application/json
    payload: '{"cmd":"notify","data":{"backlight":"{{backlight}}","backlight_max":"{{backlightMax}}","repeat":"{{repeat}}","audio_url":"{{audioUrl}}","text":"{{text}}","volume":"{{volume}}"},"hostname":"{{hostname}}"}'
  willow_notify_all:
    url: http://[your was server]:8502/api/client?action=notify
    method: POST
    content_type: application/json
    payload: '{"cmd":"notify","data":{"backlight":"{{backlight}}","backlight_max":"{{backlightMax}}","repeat":"{{repeat}}","audio_url":"{{audioUrl}}","text":"{{text}}","volume":"{{volume}}"}}'

The basics of how this works is …
timer.auto* is our countdown and trigger for completion.
input_text.auto* holds our timer name along with the willow name for respone.

So, I am going to paste pieces of code from the top down with explanations as I go. Then, at the bottom, I’ll post the full code.

These are these commands for the automation. I don’t care enough to do hours/minutes/seconds to set timers. Only minutes. The wife will learn that an hour ten is 70 minutes … or not :slight_smile:

Still over the 32k limit …

alias: Auto Timer
description: ""
trigger:
  - platform: conversation
    command:
      - (start|set|reset) {timer_time} minute {timer_name} timer [{location}]
      - (start|set|reset) {timer_name} timer (to|for) {timer_time} minute[s] [{location}]
      - (cancel|stop|end) {timer_name} timer
      - pause {timer_name} timer
      - restart {timer_name} timer
      - list timers
      - commands for timer
    id: command

The next trigger is for HA restart to notify of any timers that finished while rebooting.

  - platform: homeassistant
    event: start
    id: ha_restart

Then a finished/cancelled event triggers along with a condition that only lets the ‘auto’ timers cancelled/finished process.

  - platform: event
    event_type: timer.cancelled
    id: cancelled
  - platform: event
    event_type: timer.finished
    id: finished
condition:
  - condition: template
    value_template: "{{ trigger.id in ('command', 'ha_restart') or
                       (trigger.id in ('cancelled', 'finished') and
                        trigger.event.data.entity_id.startswith('timer.auto')) }}"

Set the variables for each Willow device. This may change in the future. They claim to be able to pass device into HA in the future (unless I read things wrong). The locations will be used to define where the output of ‘finished’ timers will announce. Other responses are returned to the device they were made. These values can be obtained from your own WAS server.

Later we look for location matching what is ‘said’. If not matched, we reply to all devices on timer.finished.

action:
  - variables:
      willows:
        - location: office
          willow_id: willow-xxxxxxxx
        - location: kitchen
          willow_id: willow-xxxxxxxx
        - location: mine
          willow_id: willow-xxxxxxxx
        - location: living room
          willow_id: willow-xxxxxxxx
        - location: hers
          willow_id: willow-xxxxxxxx
        - location: garage
          willow_id: willow-xxxxxxxx

Okay, the first choose. Here we check the first set of commands (start and set). Here, we check to see if there is an open timer available. We also look to see if the timer name is already used. If all clear, set it up and start it.

  - choose:
      - conditions:
          - condition: trigger
            id:
              - command
        sequence:
          - variables:
              command: "{{ trigger.sentence.split(' ')[0] | lower }}"
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ command in ('start', 'set') }}"
                sequence:
                  - variables:
                      timer_available: |-
                        {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'eq', 'idle') | map(attribute='entity_id') | list | count > 0 }}
                      timer_used: |-
                        {{ states.input_text | selectattr('entity_id', 'match', 'input_text.auto*') | selectattr('state', 'eq', trigger.slots.timer_name) | map(attribute='entity_id') | list | count > 0 }}
                      timer_id: |-
                        {%- if timer_available -%}
                          {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'eq', 'idle') | map(attribute='entity_id') | list | first }}
                        {%- endif -%}
                      willow: |-
                        {{ willows | selectattr('location', 'eq', trigger.slots.location) | map(attribute='willow_id') | list | first }}
                  - if:
                      - condition: template
                        value_template: "{{ timer_used }}"
                    then:
                      - set_conversation_response: |-
                          {{ trigger.slots.timer_name }} is already in use.
                    else:
                      - if:
                          - condition: template
                            value_template: "{{ timer_available }}"
                        then:
                          - service: timer.start
                            target:
                              entity_id: "{{ timer_id }}"
                            data:
                              duration: "{{ trigger.slots.timer_time | int(default=0) * 60 }}"
                          - service: input_text.set_value
                            target:
                              entity_id: "{{ timer_id | replace('timer', 'input_text') }}"
                            data:
                              value: |-
                                {{ trigger.slots.timer_name ~ '|' ~ willow if trigger.slots.location else trigger.slots.timer_name ~ '|' }}
                          - set_conversation_response: |-
                              {{ trigger.slots.timer_name }} timer started for {{ trigger.slots.timer_time }} minute{{ 's' if trigger.slots.timer_time | int(default=0) > 1 else '' }}
                        else:
                          - set_conversation_response: No timers available

Next, we check the rest of the timer commands and do what is needed.

              - conditions:
                  - condition: template
                    value_template: >-
                      {{ command in ('cancel', 'stop', 'end', 'pause', 'restart', 'reset') }}
                sequence:
                  - variables:
                      timer_set: |-
                        {{ states.input_text | selectattr('entity_id', 'match','input_text.auto*') | selectattr('state', 'match',trigger.slots.timer_name ~ '*') | map(attribute='entity_id') | list | count > 0 }}
                      timer_id: |-
                        {%- if timer_set -%}
                          {{ states.input_text | selectattr('entity_id', 'match', 'input_text.auto*') | selectattr('state', 'match', trigger.slots.timer_name ~ '*') | map(attribute='entity_id') | list | first }}
                        {%- endif -%}
                      response_command: |-
                        {%- if command in ('cancel', 'stop', 'end', 'reset') -%}
                          cancel
                        {%- elif command == 'pause' -%}
                          pause
                        {%- elif command == 'restart' -%}
                          start
                        {%- endif -%}
                      response_word: |-
                        {%- if command == 'cancel' -%}
                          cancelled
                        {%- elif command == 'pause' -%}
                          paused
                        {%- elif command == 'restart' -%}
                          restarted
                        {%- elif command == 'reset' -%}
                          reset to {{ trigger.slots.timer_time }} minutes
                        {%- endif -%}
                  - if:
                      - condition: template
                        value_template: "{{ timer_set }}"
                    then:
                      - service: timer.{{ response_command }}
                        target:
                          entity_id: "{{ timer_id | replace('input_text', 'timer') }}"
                      - if:
                          - condition: template
                            value_template: "{{ command == 'cancel' }}"
                        then:
                          - service: input_text.set_value
                            target:
                              entity_id: "{{ timer_id | replace('timer', 'input_text') }}"
                            data:
                              value: "{{ timer_id.split('.')[1] }}"
                      - if:
                          - condition: template
                            value_template: "{{ command == 'reset' }}"
                        then:
                          - service: timer.start
                            target:
                              entity_id: "{{ timer_id | replace('input_text', 'timer') }}"
                            data:
                              duration: "{{ trigger.slots.timer_time | int(default=0) * 60 }}"
                      - set_conversation_response: >-
                          {{ trigger.slots.timer_name }} timer {{ response_word }}
                    else:
                      - set_conversation_response: >-
                          No timer named {{ trigger.slots.timer_name }} is running

Still over … Jeez :slight_smile:

Then, list all running timers.

              - conditions:
                  - condition: template
                    value_template: "{{ trigger.sentence == 'list timers' }}"
                sequence:
                  - variables:
                      timers_set: >-
                        {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'ne', 'idle') | map(attribute='entity_id') | list | count > 0 }}
                      timer_ids: |-
                        {%- if timers_set -%}
                          {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'ne', 'idle') | map(attribute='entity_id') | list }}
                        {%- endif -%}
                  - if:
                      - condition: template
                        value_template: "{{ timers_set }}"
                    then:
                      - set_conversation_response: |-
                          {%- for timer in timer_ids %}
                            {%- set timer_text = '' -%}
                            {%- set timer_left = (as_datetime(state_attr(timer, 'finishes_at')) - now()).total_seconds() -%}
                            {%- set timer_hours = timer_left | timestamp_custom('%H', false) | int -%}
                            {%- set timer_minutes = timer_left | timestamp_custom('%M', false) | int -%}
                            {%- set timer_seconds = timer_left | timestamp_custom('%S', false) | int -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_hours ~ ' hour' if timer_hours > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_hours > 1 else timer_text -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_minutes ~ ' minute' if timer_minutes > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_minutes > 1 else timer_text -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_seconds ~ ' second' if timer_seconds > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_seconds > 1 else timer_text %}
                            {{ states(timer | replace('timer', 'input_text')).split('|')[0] }} timer has{{ timer_text }} left.
                          {%- endfor -%}
                    else:
                      - set_conversation_response: No timers are running

Show all timer commands to persistent notifications.

              - conditions:
                  - condition: template
                    value_template: "{{ trigger.sentence.startswith('commands for timer') }}"
                sequence:
                  - service: notify.persistent_notification
                    data:
                      message: |-
                        (start|set|reset) {timer_time} minute {timer_name} timer [{location}]
                        (start|set|reset) {timer_name} timer (to|for) {timer_time} minute[s] [{location}]
                        (cancel|stop|end) {timer_name} timer
                        pause {timer_name} timer
                        restart {timer_name} timer
                        list timers
                        commands for timer
                  - set_conversation_response: Timer commands sent to persistent notifications

On HA restart, loop through the input_text.auto* with saved off timer names and report any timers set to idle that they completed.

      - conditions:
          - condition: trigger
            id:
              - ha_restart
        sequence:
          - variables:
              timers: "{{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'eq', 'idle') | map(attribute='name') | list }}"
          - repeat:
              for_each: "{{ states.input_text | selectattr('entity_id', 'match', 'input_text.auto*') | rejectattr('state', 'match', 'auto*') | selectattr('name', 'in', timers) | map(attribute='entity_id') | list }}"
              sequence:
                - variables:
                    willow: "{{ states(repeat.item).split('|')[1] }}"
                - if:
                    - condition: template
                      value_template: "{{ willow == '' }}"
                  then:
                    - service: rest_command.willow_notify_all
                      data:
                        audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(repeat.item).split('|')[0] ~ '+timer+finished' }}"
                        backlight: true
                        backlightMax: true
                        repeat: 1
                        text: "{{ states(repeat.item).split('|')[0][0] | upper ~ states(repeat.item).split('|')[0][1:] | lower ~ ' timer finished' }}"
                        volume: 100
                  else:
                    - service: rest_command.willow_notify
                      data:
                        audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(repeat.item).split('|')[0] ~ '+timer+finished' }}"
                        hostname: "{{ willow }}"
                        backlight: true
                        backlightMax: true
                        repeat: 1
                        text: "{{ states(repeat.item).split('|')[0][0] | upper ~ states(repeat.item).split('|')[0][1:] | lower ~ ' timer finished' }}"
                        volume: 100
                - service: timer.start
                  target:
                    entity_id: "{{ repeat.item | replace('input_box', 'timer') }}"
                  data:
                    duration: "60"
                  enabled: false
                - service: input_text.set_value
                  target:
                    entity_id: "{{ repeat.item }}"
                  data:
                    value: "{{ repeat.item.split('.')[1] }}"

Then, timer finished

      - conditions:
          - condition: trigger
            id:
              - finished
        sequence:
          - variables:
              willow: "{{ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[1] }}"
          - if:
              - condition: template
                value_template: "{{ willow == '' }}"
            then:
              - service: rest_command.willow_notify_all
                data:
                  audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0] ~ '+timer+finished' }}"
                  backlight: true
                  backlightMax: true
                  repeat: 1
                  text: "{{ states(trigger.event.data.entity_id | replace('timer', 'input_text'))[0] | upper ~states(trigger.event.data.entity_id | replace('timer', 'input_text'))[1:] | lower ~ ' timer finished' }}"
                  volume: 100
            else:
              - service: rest_command.willow_notify
                data:
                  audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0] ~ '+timer+finished' }}"
                  hostname: "{{ willow }}"
                  backlight: true
                  backlightMax: true
                  repeat: 1
                  text: "{{ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0][0] | upper ~states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0][1:] | lower ~ ' timer finished' }}"
                  volume: 100
          - service: timer.start
            target:
              entity_id: "{{ trigger.event.data.entity_id }}"
            data:
              duration: "60"
            enabled: false
          - service: input_text.set_value
            target:
              entity_id: "{{ trigger.event.data.entity_id | replace('timer', 'input_text') }}"
            data:
              value: "{{ trigger.event.data.entity_id.split('.')[1] }}"

And timer cancelled

      - conditions:
          - condition: trigger
            id:
              - cancelled
        sequence:
          - service: input_text.set_value
            target:
              entity_id: |-
                {{ trigger.event.data.entity_id | replace('timer', 'input_text') }}
            data:
              value: "{{ trigger.event.data.entity_id.split('.')[1] }}"

Finally, single mode. There is some duplicated code above based on needing to do single mode. Conversation response does not get a response when mode: queued
There is no data when response_variable in scripts in mode: queued · Issue #104218 · home-assistant/core (github.com)

mode: single

I think I got everything in here. I’ll update this as I (or you folks) fined issues and get them fixed.

Here is the full code

alias: Auto Timer
description: ""
trigger:
  - platform: conversation
    command:
      - (start|set|reset) {timer_time} minute {timer_name} timer [{location}]
      - (start|set|reset) {timer_name} timer (to|for) {timer_time} minute[s] [{location}]
      - (cancel|stop|end) {timer_name} timer
      - pause {timer_name} timer
      - restart {timer_name} timer
      - list timers
      - commands for timer
    id: command
  - platform: homeassistant
    event: start
    id: ha_restart
  - platform: event
    event_type: timer.cancelled
    id: cancelled
  - platform: event
    event_type: timer.finished
    id: finished
condition:
  - condition: template
    value_template: "{{ trigger.id in ('command', 'ha_restart') or
                       (trigger.id in ('cancelled', 'finished') and
                        trigger.event.data.entity_id.startswith('timer.auto')) }}"
action:
  - variables:
      willows:
        - location: office
          willow_id: willow-3030f95aa8bc
        - location: kitchen
          willow_id: willow-3030f95a94b4
        - location: mine
          willow_id: willow-3030f95ad088
        - location: living room
          willow_id: willow-3030f95ac698
        - location: hers
          willow_id: willow-3030f95a9234
        - location: garage
          willow_id: willow-3030f95a94b8
  - choose:
      - conditions:
          - condition: trigger
            id:
              - command
        sequence:
          - variables:
              command: "{{ trigger.sentence.split(' ')[0] | lower }}"
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ command in ('start', 'set') }}"
                sequence:
                  - variables:
                      timer_available: |-
                        {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'eq', 'idle') | map(attribute='entity_id') | list | count > 0 }}
                      timer_used: |-
                        {{ states.input_text | selectattr('entity_id', 'match', 'input_text.auto*') | selectattr('state', 'eq', trigger.slots.timer_name) | map(attribute='entity_id') | list | count > 0 }}
                      timer_id: |-
                        {%- if timer_available -%}
                          {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'eq', 'idle') | map(attribute='entity_id') | list | first }}
                        {%- endif -%}
                      willow: |-
                        {{ willows | selectattr('location', 'eq', trigger.slots.location) | map(attribute='willow_id') | list | first }}
                  - if:
                      - condition: template
                        value_template: "{{ timer_used }}"
                    then:
                      - set_conversation_response: |-
                          {{ trigger.slots.timer_name }} is already in use.
                    else:
                      - if:
                          - condition: template
                            value_template: "{{ timer_available }}"
                        then:
                          - service: timer.start
                            target:
                              entity_id: "{{ timer_id }}"
                            data:
                              duration: "{{ trigger.slots.timer_time | int(default=0) * 60 }}"
                          - service: input_text.set_value
                            target:
                              entity_id: "{{ timer_id | replace('timer', 'input_text') }}"
                            data:
                              value: |-
                                {{ trigger.slots.timer_name ~ '|' ~ willow if trigger.slots.location else trigger.slots.timer_name ~ '|' }}
                          - set_conversation_response: |-
                              {{ trigger.slots.timer_name }} timer started for {{ trigger.slots.timer_time }} minute{{ 's' if trigger.slots.timer_time | int(default=0) > 1 else '' }}
                        else:
                          - set_conversation_response: No timers available
              - conditions:
                  - condition: template
                    value_template: >-
                      {{ command in ('cancel', 'stop', 'end', 'pause', 'restart', 'reset') }}
                sequence:
                  - variables:
                      timer_set: |-
                        {{ states.input_text | selectattr('entity_id', 'match','input_text.auto*') | selectattr('state', 'match',trigger.slots.timer_name ~ '*') | map(attribute='entity_id') | list | count > 0 }}
                      timer_id: |-
                        {%- if timer_set -%}
                          {{ states.input_text | selectattr('entity_id', 'match', 'input_text.auto*') | selectattr('state', 'match', trigger.slots.timer_name ~ '*') | map(attribute='entity_id') | list | first }}
                        {%- endif -%}
                      response_command: |-
                        {%- if command in ('cancel', 'stop', 'end', 'reset') -%}
                          cancel
                        {%- elif command == 'pause' -%}
                          pause
                        {%- elif command == 'restart' -%}
                          start
                        {%- endif -%}
                      response_word: |-
                        {%- if command == 'cancel' -%}
                          cancelled
                        {%- elif command == 'pause' -%}
                          paused
                        {%- elif command == 'restart' -%}
                          restarted
                        {%- elif command == 'reset' -%}
                          reset to {{ trigger.slots.timer_time }} minutes
                        {%- endif -%}
                  - if:
                      - condition: template
                        value_template: "{{ timer_set }}"
                    then:
                      - service: timer.{{ response_command }}
                        target:
                          entity_id: "{{ timer_id | replace('input_text', 'timer') }}"
                      - if:
                          - condition: template
                            value_template: "{{ command == 'cancel' }}"
                        then:
                          - service: input_text.set_value
                            target:
                              entity_id: "{{ timer_id | replace('timer', 'input_text') }}"
                            data:
                              value: "{{ timer_id.split('.')[1] }}"
                      - if:
                          - condition: template
                            value_template: "{{ command == 'reset' }}"
                        then:
                          - service: timer.start
                            target:
                              entity_id: "{{ timer_id | replace('input_text', 'timer') }}"
                            data:
                              duration: "{{ trigger.slots.timer_time | int(default=0) * 60 }}"
                      - set_conversation_response: >-
                          {{ trigger.slots.timer_name }} timer {{ response_word }}
                    else:
                      - set_conversation_response: >-
                          No timer named {{ trigger.slots.timer_name }} is running
              - conditions:
                  - condition: template
                    value_template: "{{ trigger.sentence == 'list timers' }}"
                sequence:
                  - variables:
                      timers_set: >-
                        {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'ne', 'idle') | map(attribute='entity_id') | list | count > 0 }}
                      timer_ids: |-
                        {%- if timers_set -%}
                          {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'ne', 'idle') | map(attribute='entity_id') | list }}
                        {%- endif -%}
                  - if:
                      - condition: template
                        value_template: "{{ timers_set }}"
                    then:
                      - set_conversation_response: |-
                          {%- for timer in timer_ids %}
                            {%- set timer_text = '' -%}
                            {%- set timer_left = (as_datetime(state_attr(timer, 'finishes_at')) - now()).total_seconds() -%}
                            {%- set timer_hours = timer_left | timestamp_custom('%H', false) | int -%}
                            {%- set timer_minutes = timer_left | timestamp_custom('%M', false) | int -%}
                            {%- set timer_seconds = timer_left | timestamp_custom('%S', false) | int -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_hours ~ ' hour' if timer_hours > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_hours > 1 else timer_text -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_minutes ~ ' minute' if timer_minutes > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_minutes > 1 else timer_text -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_seconds ~ ' second' if timer_seconds > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_seconds > 1 else timer_text %}
                            {{ states(timer | replace('timer', 'input_text')).split('|')[0] }} timer has{{ timer_text }} left.
                          {%- endfor -%}
                    else:
                      - set_conversation_response: No timers are running
              - conditions:
                  - condition: template
                    value_template: "{{ trigger.sentence.startswith('commands for timer') }}"
                sequence:
                  - service: notify.persistent_notification
                    data:
                      message: |-
                        (start|set|reset) {timer_time} minute {timer_name} timer [{location}]
                        (start|set|reset) {timer_name} timer (to|for) {timer_time} minute[s] [{location}]
                        (cancel|stop|end) {timer_name} timer
                        pause {timer_name} timer
                        restart {timer_name} timer
                        list timers
                        commands for timer
                  - set_conversation_response: Timer commands sent to persistent notifications
      - conditions:
          - condition: trigger
            id:
              - ha_restart
        sequence:
          - variables:
              timers: "{{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'eq', 'idle') | map(attribute='name') | list }}"
          - repeat:
              for_each: "{{ states.input_text | selectattr('entity_id', 'match', 'input_text.auto*') | rejectattr('state', 'match', 'auto*') | selectattr('name', 'in', timers) | map(attribute='entity_id') | list }}"
              sequence:
                - variables:
                    willow: "{{ states(repeat.item).split('|')[1] }}"
                - if:
                    - condition: template
                      value_template: "{{ willow == '' }}"
                  then:
                    - service: rest_command.willow_notify_all
                      data:
                        audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(repeat.item).split('|')[0] ~ '+timer+finished' }}"
                        backlight: true
                        backlightMax: true
                        repeat: 1
                        text: "{{ states(repeat.item).split('|')[0][0] | upper ~ states(repeat.item).split('|')[0][1:] | lower ~ ' timer finished' }}"
                        volume: 100
                  else:
                    - service: rest_command.willow_notify
                      data:
                        audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(repeat.item).split('|')[0] ~ '+timer+finished' }}"
                        hostname: "{{ willow }}"
                        backlight: true
                        backlightMax: true
                        repeat: 1
                        text: "{{ states(repeat.item).split('|')[0][0] | upper ~ states(repeat.item).split('|')[0][1:] | lower ~ ' timer finished' }}"
                        volume: 100
                - service: timer.start
                  target:
                    entity_id: "{{ repeat.item | replace('input_box', 'timer') }}"
                  data:
                    duration: "60"
                  enabled: false
                - service: input_text.set_value
                  target:
                    entity_id: "{{ repeat.item }}"
                  data:
                    value: "{{ repeat.item.split('.')[1] }}"
      - conditions:
          - condition: trigger
            id:
              - finished
        sequence:
          - variables:
              willow: "{{ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[1] }}"
          - if:
              - condition: template
                value_template: "{{ willow == '' }}"
            then:
              - service: rest_command.willow_notify_all
                data:
                  audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0] ~ '+timer+finished' }}"
                  backlight: true
                  backlightMax: true
                  repeat: 1
                  text: "{{ states(trigger.event.data.entity_id | replace('timer', 'input_text'))[0] | upper ~states(trigger.event.data.entity_id | replace('timer', 'input_text'))[1:] | lower ~ ' timer finished' }}"
                  volume: 100
            else:
              - service: rest_command.willow_notify
                data:
                  audioUrl: "{{ 'https://wis.jeffcrum.com:19000/api/tts?text=' ~ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0] ~ '+timer+finished' }}"
                  hostname: "{{ willow }}"
                  backlight: true
                  backlightMax: true
                  repeat: 1
                  text: "{{ states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0][0] | upper ~states(trigger.event.data.entity_id | replace('timer', 'input_text')).split('|')[0][1:] | lower ~ ' timer finished' }}"
                  volume: 100
          - service: timer.start
            target:
              entity_id: "{{ trigger.event.data.entity_id }}"
            data:
              duration: "60"
            enabled: false
          - service: input_text.set_value
            target:
              entity_id: "{{ trigger.event.data.entity_id | replace('timer', 'input_text') }}"
            data:
              value: "{{ trigger.event.data.entity_id.split('.')[1] }}"
      - conditions:
          - condition: trigger
            id:
              - cancelled
        sequence:
          - service: input_text.set_value
            target:
              entity_id: |-
                {{ trigger.event.data.entity_id | replace('timer', 'input_text') }}
            data:
              value: "{{ trigger.event.data.entity_id.split('.')[1] }}"
mode: single

Please let me know if you find any issues that we can get fixed or any code that can be optimized.

Of course, I am sure I can fix words in my descriptions as this was built over time and I probably missed something in my changes too.

All criticisms are welcome!

Awesome work. Now i just need a slick automation to play music and I’m covering 90% of my Alexa use cases.

1 Like

Thanks.

Pretty sure these devices are not how you want to play music. But, enjoy the timers and let me know if there are any issues.

Nah i just meant “alexa, play the beatles on the kitchen speakers” which would activate music assistant to play music on an external speaker.

Gotcha.

I think there are examples of doing that on here.

Also, please let me know if you run into any issues.

The one I see every once in a while, is Willow will send:
reset oven timer to one minute

Instead of:
reset oven timer to 1 minute

We could fix that in HA. But, I am waiting for WAC to be built into WAS and fix it at the front end.

Nice work, @jeffcrum!
I’m glad someone picked up where I left off, I’m lacking some time to improve mine. Good stuff!

1 Like

Thanks. It was fun to do. I hope someone can use it, parts of it, or build on it.

1 Like

Hey @jeffcrum i am working on getting this implemented but when i set a timer it immediately finishes for some reason. I have an input_text named input_text.autoKitchen and a timer named timer.autoKitchen. I dont really need the willow integration here as i updated the actions to instead send TTS over a couple speakers in my house as well as notify my wife and i on our phones. How much work would it be to effectively remove all willow integration stuff?

Heres my existing automation:

alias: Auto Timer
description: ""
trigger:
  - platform: conversation
    command:
      - >-
        (start|set|reset) (a) {timer_time} minute {timer_name} timer
        [{location}]
      - >-
        (start|set|reset) (a) {timer_name} timer (to|for) {timer_time} minute[s]
        [{location}]
      - (cancel|stop|end) {timer_name} timer
      - pause {timer_name} timer
      - restart {timer_name} timer
      - list timers
      - commands for timer
      - (start|set|reset) (a) timer (to|for) {timer_time} minute[s]
    id: command
  - platform: homeassistant
    event: start
    id: ha_restart
  - platform: event
    event_type: timer.cancelled
    id: cancelled
  - platform: event
    event_type: timer.finished
    id: finished
condition:
  - condition: template
    value_template: >-
      {{ trigger.id in ('command', 'ha_restart') or (trigger.id in ('cancelled',
      'finished') and trigger.event.data.entity_id.startswith('timer.auto')) }}
action:
  - variables:
      willows:
        - location: kitchen
          willow_id: willow-84fce66405bc
  - choose:
      - conditions:
          - condition: trigger
            id:
              - command
        sequence:
          - variables:
              command: "{{ trigger.sentence.split(' ')[0] | lower }}"
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ command in ('start', 'set') }}"
                sequence:
                  - variables:
                      timer_available: >-
                        {{ states.timer | selectattr('entity_id', 'match',
                        'timer.auto*') | selectattr('state', 'eq', 'idle') |
                        map(attribute='entity_id') | list | count > 0 }}
                      timer_used: >-
                        {{ states.input_text | selectattr('entity_id', 'match',
                        'input_text.auto*') | selectattr('state', 'eq',
                        trigger.slots.timer_name) | map(attribute='entity_id') |
                        list | count > 0 }}
                      timer_id: |-
                        {%- if timer_available -%}
                          {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'eq', 'idle') | map(attribute='entity_id') | list | first }}
                        {%- endif -%}
                      willow: >-
                        {{ willows | selectattr('location', 'eq',
                        trigger.slots.location) | map(attribute='willow_id') |
                        list | first }}
                  - if:
                      - condition: template
                        value_template: "{{ timer_used }}"
                    then:
                      - set_conversation_response: "{{ trigger.slots.timer_name }} is already in use."
                    else:
                      - if:
                          - condition: template
                            value_template: "{{ timer_available }}"
                        then:
                          - service: timer.start
                            target:
                              entity_id: "{{ timer_id }}"
                            data:
                              duration: >-
                                {{ trigger.slots.timer_time | int(default=0) *
                                60 }}
                          - service: input_text.set_value
                            target:
                              entity_id: "{{ timer_id | replace('timer', 'input_text') }}"
                            data:
                              value: >-
                                {{ trigger.slots.timer_name ~ '|' ~ willow if
                                trigger.slots.location else
                                trigger.slots.timer_name ~ '|' }}
                          - set_conversation_response: >-
                              {{ trigger.slots.timer_name }} timer started for
                              {{ trigger.slots.timer_time }} minute{{ 's' if
                              trigger.slots.timer_time | int(default=0) > 1 else
                              '' }}
                        else:
                          - set_conversation_response: No timers available
              - conditions:
                  - condition: template
                    value_template: >-
                      {{ command in ('cancel', 'stop', 'end', 'pause',
                      'restart', 'reset') }}
                sequence:
                  - variables:
                      timer_set: >-
                        {{ states.input_text | selectattr('entity_id',
                        'match','input_text.auto*') | selectattr('state',
                        'match',trigger.slots.timer_name ~ '*') |
                        map(attribute='entity_id') | list | count > 0 }}
                      timer_id: |-
                        {%- if timer_set -%}
                          {{ states.input_text | selectattr('entity_id', 'match', 'input_text.auto*') | selectattr('state', 'match', trigger.slots.timer_name ~ '*') | map(attribute='entity_id') | list | first }}
                        {%- endif -%}
                      response_command: |-
                        {%- if command in ('cancel', 'stop', 'end', 'reset') -%}
                          cancel
                        {%- elif command == 'pause' -%}
                          pause
                        {%- elif command == 'restart' -%}
                          start
                        {%- endif -%}
                      response_word: |-
                        {%- if command == 'cancel' -%}
                          cancelled
                        {%- elif command == 'pause' -%}
                          paused
                        {%- elif command == 'restart' -%}
                          restarted
                        {%- elif command == 'reset' -%}
                          reset to {{ trigger.slots.timer_time }} minutes
                        {%- endif -%}
                  - if:
                      - condition: template
                        value_template: "{{ timer_set }}"
                    then:
                      - service: timer.{{ response_command }}
                        target:
                          entity_id: "{{ timer_id | replace('input_text', 'timer') }}"
                      - if:
                          - condition: template
                            value_template: "{{ command == 'cancel' }}"
                        then:
                          - service: input_text.set_value
                            target:
                              entity_id: "{{ timer_id | replace('timer', 'input_text') }}"
                            data:
                              value: "{{ timer_id.split('.')[1] }}"
                      - if:
                          - condition: template
                            value_template: "{{ command == 'reset' }}"
                        then:
                          - service: timer.start
                            target:
                              entity_id: "{{ timer_id | replace('input_text', 'timer') }}"
                            data:
                              duration: >-
                                {{ trigger.slots.timer_time | int(default=0) *
                                60 }}
                      - set_conversation_response: >-
                          {{ trigger.slots.timer_name }} timer {{ response_word
                          }}
                    else:
                      - set_conversation_response: >-
                          No timer named {{ trigger.slots.timer_name }} is
                          running
              - conditions:
                  - condition: template
                    value_template: "{{ trigger.sentence == 'list timers' }}"
                sequence:
                  - variables:
                      timers_set: >-
                        {{ states.timer | selectattr('entity_id', 'match',
                        'timer.auto*') | selectattr('state', 'ne', 'idle') |
                        map(attribute='entity_id') | list | count > 0 }}
                      timer_ids: |-
                        {%- if timers_set -%}
                          {{ states.timer | selectattr('entity_id', 'match', 'timer.auto*') | selectattr('state', 'ne', 'idle') | map(attribute='entity_id') | list }}
                        {%- endif -%}
                  - if:
                      - condition: template
                        value_template: "{{ timers_set }}"
                    then:
                      - set_conversation_response: |-
                          {%- for timer in timer_ids %}
                            {%- set timer_text = '' -%}
                            {%- set timer_left = (as_datetime(state_attr(timer, 'finishes_at')) - now()).total_seconds() -%}
                            {%- set timer_hours = timer_left | timestamp_custom('%H', false) | int -%}
                            {%- set timer_minutes = timer_left | timestamp_custom('%M', false) | int -%}
                            {%- set timer_seconds = timer_left | timestamp_custom('%S', false) | int -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_hours ~ ' hour' if timer_hours > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_hours > 1 else timer_text -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_minutes ~ ' minute' if timer_minutes > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_minutes > 1 else timer_text -%}
                            {%- set timer_text = timer_text ~ ' ' ~ timer_seconds ~ ' second' if timer_seconds > 0 else timer_text -%}
                            {%- set timer_text = timer_text ~ 's' if timer_seconds > 1 else timer_text %}
                            {{ states(timer | replace('timer', 'input_text')).split('|')[0] }} timer has{{ timer_text }} left.
                          {%- endfor -%}
                    else:
                      - set_conversation_response: No timers are running
              - conditions:
                  - condition: template
                    value_template: "{{ trigger.sentence.startswith('commands for timer') }}"
                sequence:
                  - service: notify.persistent_notification
                    data:
                      message: >-
                        (start|set|reset) {timer_time} minute {timer_name} timer
                        [{location}]

                        (start|set|reset) {timer_name} timer (to|for)
                        {timer_time} minute[s] [{location}]

                        (cancel|stop|end) {timer_name} timer

                        pause {timer_name} timer

                        restart {timer_name} timer

                        list timers

                        commands for timer
                  - set_conversation_response: Timer commands sent to persistent notifications
      - conditions:
          - condition: trigger
            id:
              - ha_restart
        sequence:
          - variables:
              timers: >-
                {{ states.timer | selectattr('entity_id', 'match',
                'timer.auto*') | selectattr('state', 'eq', 'idle') |
                map(attribute='name') | list }}
          - repeat:
              for_each: >-
                {{ states.input_text | selectattr('entity_id', 'match',
                'input_text.auto*') | rejectattr('state', 'match', 'auto*') |
                selectattr('name', 'in', timers) | map(attribute='entity_id') |
                list }}
              sequence:
                - variables:
                    willow: "{{ states(repeat.item).split('|')[1] }}"
                - service: timer.start
                  target:
                    entity_id: "{{ repeat.item | replace('input_box', 'timer') }}"
                  data:
                    duration: "60"
                  enabled: false
                - service: tts.speak
                  metadata: {}
                  data:
                    cache: false
                    media_player_entity_id: media_player.the_kitchen
                    message: >-
                      {{ states(repeat.item).split('|')[0][0] | upper ~
                      states(repeat.item).split('|')[0][1:] | lower ~ ' timer
                      finished' }}
                  target:
                    entity_id: tts.piper
                - service: tts.speak
                  metadata: {}
                  data:
                    cache: false
                    media_player_entity_id: media_player.master_bedroom
                    message: >-
                      {{ states(repeat.item).split('|')[0][0] | upper ~
                      states(repeat.item).split('|')[0][1:] | lower ~ ' timer
                      finished' }}
                  target:
                    entity_id: tts.piper
                - service: notify.parents
                  data:
                    message: >-
                      {{ states(repeat.item).split('|')[0][0] | upper ~
                      states(repeat.item).split('|')[0][1:] | lower ~ ' timer
                      finished' }}
                    title: Timer!
                - service: input_text.set_value
                  target:
                    entity_id: "{{ repeat.item }}"
                  data:
                    value: "{{ repeat.item.split('.')[1] }}"
      - conditions:
          - condition: trigger
            id:
              - finished
        sequence:
          - variables:
              willow: >-
                {{ states(trigger.event.data.entity_id | replace('timer',
                'input_text')).split('|')[1] }}
          - service: tts.speak
            metadata: {}
            data:
              cache: false
              media_player_entity_id: media_player.the_kitchen
              message: Timer Finished.
            target:
              entity_id: tts.piper
          - service: tts.speak
            metadata: {}
            data:
              cache: false
              media_player_entity_id: media_player.master_bedroom
              message: Timer Finished.
            target:
              entity_id: tts.piper
          - service: notify.parents
            data:
              message: Timer Finished
              title: Timer!
          - service: timer.start
            target:
              entity_id: "{{ trigger.event.data.entity_id }}"
            data:
              duration: "60"
            enabled: false
          - service: input_text.set_value
            target:
              entity_id: >-
                {{ trigger.event.data.entity_id | replace('timer', 'input_text')
                }}
            data:
              value: "{{ trigger.event.data.entity_id.split('.')[1] }}"
      - conditions:
          - condition: trigger
            id:
              - cancelled
        sequence:
          - service: input_text.set_value
            target:
              entity_id: >-
                {{ trigger.event.data.entity_id | replace('timer', 'input_text')
                }}
            data:
              value: "{{ trigger.event.data.entity_id.split('.')[1] }}"
          - service: tts.speak
            metadata: {}
            data:
              cache: false
              media_player_entity_id: media_player.the_kitchen
              message: Timer Cancelled.
            target:
              entity_id: tts.piper
          - service: tts.speak
            metadata: {}
            data:
              cache: false
              media_player_entity_id: media_player.master_bedroom
              message: Timer Cancelled.
            target:
              entity_id: tts.piper
          - service: notify.parents
            data:
              message: Timer Cancelled
              title: Timer!
mode: single

This is way overkill if you just want a kitchen timer to be controlled by voice and not using the willow devices.

You won’t need to search for an open timer. You won’t need to store the ‘named’ timer in the input_text along with the willow named device. You won’t even need the input_text. None of the loops are needed.

I’d suggest starting a new automation from scratch and grab the parts out of my code to build the pieces you need as you go.

But, back to your original issue … what does the trace show you? Is it getting your sentence correct? It is setting the timer. But, not setting the correct length. Check the variables in the trace and see how they are setting.

Yeah thats what i figured. I’ll try and do that and see how it goes. I think the issue is that its hearing “five minutes” and it tries to set the timer for “five” instead of “5” and that makes the timer just think its set to 0 and finishes the timer immediately.

Yeah. That’ll do exactly what you are seeing. I have only had it happen once here. So, I didn’t put any code in to fix that. But it certainly could be coded for.

I think my willows spell the number for ten and under. Numbers over that get the digits. I seldom set a timer under ten minutes.

You could change your timer settings to be five minutes. Then, if it does not get a valid number, it’ll start there. Kind of a default minimum timer that way.

Ah interesting. I just tried 15 minutes and it used the number. Im testing out the other timer automation that yours is based on to see if that works but so far something isnt right there either. I’ll just have to dedicate some time on writing something that works for my use cases. We dont usually set timers under 10 minutes either but our kids do so i gotta figure out how to handle that scenario. I suppose some hashmap of word to number might do the trick or something.

Yup. Put it in a new variable. If trigger.slots.timer_time is numeric, put it in there. Otherwise, basically go from one through ten and flip them to numbers to store in there.

Then, replace trigger.slots.timer_time with your new variable in the code.

Something like

{% set t = {
  'one': 1,
  'two': 2,
  'three': 3,
  'four': 4 } %}
{{ t.get( 'two' ) }}

Thats much more concise than my impl:

{{ tenbelow[(trigger.slots.timer_time | trim | lower)] |int * 60 }}

I got all my stuff working pretty straight forward with just one timer. Now im trying to figure out how to display the countdown via esphome. It should be simple but i am struggling. Any advise?

Great news. Glad you got it working.

I don’t have any ESPHome devices. So, I can’t help there. Maybe someone will come upon the question and help or you can start a new one.

Im running esphome now on my box3-s3 instead of willow (even though i have WAS and WIS up and running). It seems that i dont necessarily need willow for decent processing and i found an integration that turns WIS into a TTS service like piper. I am sure i’ll figure it all out here. So close to getting the spouse approval rating bump after ditching all my alexa devices. music, timers, simple on/off automations is 99% the dream.

1 Like