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