silveradept: A kodama with a trombone. The trombone is playing music, even though it is held in a rest position (Default)
[personal profile] silveradept
Or: There Are No Timers Remaining

So, last we left this situation, I had managed to get a single arbitrary timer working and had a list of suggested improvements to make the arbitrary timer function more smoothly and effectively. There were three suggestions, and it turns out they're all related to each other.

So, what needed doing was:
  1. Implement logic so that when a timer was set, it would be assigned to a timer that was both (a) available and (b) on the list of timers constructed for this purpose,

  2. Play the chime associated with the selected timer and read out the name of the timer chosen and indicate it has started.

  3. Implement logic so that when there are no entities that can fill the request, it informs us of this and then reads out the amount of time remaining on the timers that are running.

  4. Ultimately, after getting these features working correctly, I had an additional goal, which was to shunt as much of the logic and other such things into a script that could be called, so that making changes to the script would not require a restart of the entire Castle to see if the changes worked.

  5. To achieve that goal, however, I had to first figure out how to collect the data that comes in from the voice assistant so it can be manipulated and then passed on further to the script previously mentioned.

Attempting to solve this problem set normally and through search engine use presented an immediate problem - a significant amount of basic Python functions can't be called from inside the templating language and extensions that are available to me. So I couldn't call str() when I needed to convert the ints that came in from the voice assistant into strings, and that means a lot of the tests that I could try for in building the logic were not available to me, not could I call most of the functions available to a regular script.

As it turned out, I had all I needed to succeed in front of me for the logic part, but I needed to understand it better and more accurately to properly use it.

First, I needed to help my own case some by separating out the arbitrary timers from other timers that I don't want their duration varies overwritten. Thankfully, Home Assistant has a type of object called a group, and groups can be closed of just about any kind of entity or multiple types of entities. Putting the arbitrary timers into their own group was easy enough.

Second, I decided to have a whack at the part of getting the speech system to read out the time remaining on the timers. This would normally bea pretty easy affair, except timers in Home Assistant don't have a start value, only an end value, stored as a datetime object. This was the first clue that there's some magic going on behind the scenes in the UI space, because running timers in the UI display their remaining time in a human readable formal.

Second, Python supports adding and subtracting datetime objects from each other, which creates timedelta objects. The templating language, not so much. But, it does support manipulations that turn datetime objects into UNIX timestamps, which are floating-point numbers, and math can be done on floats. New problem: UNIX timestamps are only in seconds. Thankfully, the modulo operation is avaipable for numbers, so with a total of seconds from the math part, we can math out the hours, minutes, and seconds remaining on a way that the speech system will understand, and from there, we wrap it in a for loop based on selecting from the list of timers which ones have their state attribute set to active. Cool.

Then came the logic parts of it, and it turns out, the logic parts are related to each other, even though I didn't see them that way at first. The first logic problem to solve was to set up a situation where only one timer would be called, even if there were multiple timers that were idle at the time. Thankfully, the documentation explicitly shows a way of expanding a group to iterate through it, and there was a method I could call to it called "first" that would return the first member of the group that matched the criteria I was looking for. So long as there was a timer that was idle, the timer would get set with the parameters padded through the voice assistant. We're 90% of the way home!

And then I hit a wall. Because all the voice I've done up to this point works only if there's a timer available. When there isn't a timer available, "first" throws an error. (Because if you ask a computer to find the first element of an empty set, it's going to look at you funny.) Being the enterprising entity that I am, I looked up how one might, in Python, test to see if a set is empty, and, well, most of those methods don't work here, because I'm not writing Python, I'm writing template engine code. I still haven't realized it at this particular point, so most of the things I'm trying to do fail annoyingly.

First, I tried to solve the "first" problem by creating a new timer in the group and setting it to duration 0, so that if it were called, it would activate and immediately stop. I believed I could manage it this way because timers have an attribute labeled "editable." If I could set the editable attribute to false, I believed I could prevent the values coming in from the voice assistant from being written to the timer. It did not work that way. "Editable" means something very different, or all timers created using the friendly UI interface are always editable. Either way, I couldn't use a dummy timer to them call the script of how much time was left when it immediately finished.

After bashing my head at that problem for a while and getting nowhere, although with very helpful suggestions to try and get the computer to tell me what's going on (and not actually then looking at the error logs when they happened, silly me), sometime who does actually do computer programming and the like took a look at the problem and eventually went and looked at the documentation for the template engine itself.

This is the point of the story where I take an apparent diversion into talking about how I lost so many points in bits and pieces on my maths assignments because I could demonstrate the capability that I knew what I was doing, but I had a bad tendency to either drop signs (forgetting to turn something negative or forgetting to turn it back to a positive) or forget to indicate what units my answers were in. The detail work that is infuriating when you discover that the reason everything went so sideways is because you missed a letter somewhere, or you lack a close-paren, or there's an indenting error somewhere that's causing the whole thing to go bad. There's some amount of help that you can get from automatic tools and from configuration-checking, but valid code can still sometime produce invalid results if the situation isn't invalid right from the get-go. For me, the lost thing that I still didn't fully understand was that I wasn't wriing Python. But, with the help of the programming friend, who looked up what kinds of tests were available so that I could catch the condition of the empty set, I finally managed to construct something that would follow the logic line of what I wanted to have happen - if the first element comes back, call the timer service and pass it the necessary parameters, if it comes back empty, then call the other script that will read out the remaining time. Testing that logic, with the correct type of test, worked properly!

And then created another problem. Because part of the intended strategy was to have the speech system read out what timer had started, and to use the correct chime for that timer. If we start the time, though, and then ask for the first idle timer, we get the next timer in squence, rather than the one we want. Thankfully, with the movement of most of the logic to an external script, it became a lot easier to order the actions so that all the actions that rely on the timer not having started yet can happen first, so they point to the correct timer, and then, after all of that, the timer can start. As it turned out, not all of the logic could be shuffled over to the new script, but instead, I had to leave the integer-to-script conversion of the time numbers in the first set of actions done before passing to the next script. Which meant, now that I had a firmer grasp on how the templating worked, meant I could understand how to pass the necessary data over to the script, and now, it's all working according to plan.

[VICTORY FANFARE GOES HERE.]

That was a lot of involved logic, looking up code snippets, seeing how they worked, then making them work the way I wanted to, an eventually coming to a greater understanding of how the whole system works so that I can accomplish something that I had thought at the outset as being something more Hello World-y in terms of expectations that any voice assistant worth its salt would be able to do. The way it's been set up, too, means that the group can be of any length, so long as I have names for the timers.

There is one optimization problem still outstanding, which is that I have things set up such that each timer has their own automation listening for when they finish to play their ending chimes. I could probably figure out how to make one automation out of it that listens for the event of any timer expiring, and then is able to chime based on which timer just signified it was done all in one automation, but I suspect the logic for it is more complicated than it appears, or I would have to learn a new part of how the Home Assistant works. It's currently easier to give each timer created its own automation to fire when it reaches zero, but if there's a spot to listen in for a timer expiring and the event that fires whan the timer expires contains the same kind of attributes that I can take advantage of when setting the timer up, it's possible that the logic involved could be abstracted so that the scripting really doesn't care at all about how big the group is, or even whether or not the sound that it's being asked to play even exists, and instead just grabs the friendly name of the timer than just expired and plays its chime.

But, at least functionally, I have, with help, successfully created the ability to set arbitrary timers with my voice, with logic in place to do something useful if all the timers are busy at the moment. And knowing how all of this done unlocked a thought about how I could take advantage of this knowledge to, say, call for a remote to send a signal to pause a program being watched, after letting the commercials run, so I could call the thing, go off to do something while the commercials run and then come back and start the program again without having to rewind (too much). Learn one thing, unlock the potential ability to do other interesting things. I was able to get the pause command encoded, but it requires a little bit of hardware movement to work more consistently and get the receiver and the sender in the same line of sight of each other.

Success and excellence and all of those things. If you're curious, he's here the workflow goes;

At the Rhasspy Assistant level, this is trained as a sentence (which will be sent as an intent) items surrounded by < and > are variables that can be referenced both internally in the sentences or externally by any other sentence. Items in parentheses are options that can be dropped into that part of the sentence that will match that sentence as your intent. Items in braces ([zed]), except for the name of the sentence, are optional. They're really helpful when you know you're going to say something a few different ways and you want to make sure that all of them point to the same thing, rather than having to remember the opposite way you coded the sentence. Curly braces ({queue}) after aomething indicate that whatever the value of the thing immediately preceding it is will be sent along to the intent handler for use on the Home Assistant side.

[SetArbitraryTimer]

hourexpr = (0..23){hours} (hour | hours)

minuteexpr = (0..100){minutes} (minute | minutes)

secondexpr = (0..100){seconds} (second | seconds)

timeexpr = ((<hourexpr>[[and] <minuteexpr>] [[and] <secondexpr>]) | <minuteexpr> [[and] <secondexpr>]) | <secondexpr>)

(start | set) [a] timer for <timeexpr>


So, with this set up, essentially what we have are three potential values to pass from the Voice Assistant to the Home Assistant. On the Home Assistant side, the first stop those valuess make is at the intent script, which will call a further script (that's the service: part) and pass it those same potential three values (that's data:), but here's where we have to reformat the integers that we do get into strings and provide default values for any of those three variables that we don't get passed from the Voice Assistant. The Jinja blocks here are testing as to whether the value we seek is defined (present) or not.

# Set an Arbitrary Timer From The Possible Available Pool.

SetArbitraryTimer:
action:
- service: script.set_arbitrary_timer
data:
hours: >
{% if hours is not defined %}
{% set hours = '00' %}
{% else %}
{% set hours = '{}'.format(hours) %}
{% endif %}
{{ hours }}

minutes: >
{% if minutes is not defined %}
{% set minutes = '00' %}
{% else %}
{% set minutes = '{}'.format(minutes) %}
{% endif %}
{{ minutes }}
seconds: >
{% if seconds is not defined %}
{% set seconds = '00' %}
{% else %}
{% set seconds = '{}'.format(seconds) %}
{% endif %}
{{ seconds }}


Finally, the script that actually runs to set the timer, where we consult the list of available timers, choose the first one that:s idle, play the sound associated with them, say which one has started, and then start the timer itself using the values of the variables that have been stringified, or, if there are no timers available, play an alert sound, indicate there are no timers available, and then call a second script to read out the time that is left on the running timers.

sequence:
- service: media_player.play_media
data:
media_content_id: >
{% set chosen = expand('group.arbitrary_timers') | selectattr('state',
'eq', 'idle') | first %}
{% if chosen is defined %}
share/{{ state_attr(chosen.entity_id , "friendly_name") }}.wav
{% else %}
share/notimers.wav
{% endif %}
media_content_type: music
target:
entity_id: media_player.vlc_telnet
- delay:
hours: 0
minutes: 0
seconds: 6
milliseconds: 0
- service: rest_command.rhasspy_speak
data:
payload: >
{% set chosen = expand('group.arbitrary_timers') | selectattr('state',
'eq', 'idle') | first %}
{% if chosen is defined %}
Timer {{ state_attr(chosen.entity_id , "friendly_name") }} Has Started
{% else %}
No Timers Are Available
{% endif %}
- delay:
hours: 0
minutes: 0
seconds: 2
milliseconds: 0
- service: >
{%- set service = expand('group.arbitrary_timers') | selectattr('state',
'eq', 'idle') | first %}
{% if service is defined -%}
{%- set service = 'timer.start' -%}
{%- else -%}
{%- set service = 'script.time_left_on_timers' -%}
{%- endif -%}
{{ service }}

data:
entity_id: >
{%- set chosen = expand('group.arbitrary_timers') | selectattr('state',
'eq', 'idle') | first -%}
{%- if chosen is defined -%}
{{ chosen.entity_id }}
{%- else -%}
{%- set chosen = '' -%}
{%- endif -%}
duration: |
{{ hours }}:{{ minutes }}:{{ seconds }}
mode: single
alias: Set Arbitrary Timer
icon: mdi:timer


And finally, here's the other script that reads out the time values, where I had to do all of that datetime conversion, timedelta construction, and maths calculations to get the values I needed so it could be read out correctly.

sequence:
- condition: or
conditions:
- condition: state
entity_id: timer.a
state: active
- condition: state
entity_id: timer.b
state: active
- condition: state
entity_id: timer.c
- condition: state
entity_id: timer.d
- service: rest_command.rhasspy_speak
data:
payload: >
{% for timer in states.timer -%}
{%- if timer.state == 'active' -%}
{%- set timeremaining = timedelta(seconds=(as_timestamp(state_attr(timer.entity_id, "finishes_at")) - as_timestamp(states('sensor.date_time_iso')))) -%}
{%- set secs = timeremaining.total_seconds() -%}
{%- set hours = int(secs / 3600) -%}
{%- set minutes = int(secs / 60) % 60 -%}
{%- set seconds = int(secs % 60) -%}
Timer {{ state_attr(timer.entity_id, "friendly_name") }} has {{ hours }} Hours {{ minutes }} minutes and {{ seconds }} seconds Remaining, {{ "\n" }}
{%- endif -%}
{%- endfor %}
- service: notify.notify
data:
title: Time Remaining On Timers
message: >
{% for timer in states.timer -%}
{%- if timer.state == 'active' -%}
{%- set timeremaining = timedelta(seconds=(as_timestamp(state_attr(timer.entity_id, "finishes_at")) - as_timestamp(states('sensor.date_time_iso')))) -%}
{%- set secs = timeremaining.total_seconds() -%}
{%- set hours = int(secs / 3600) -%}
{%- set minutes = int(secs / 60) % 60 -%}
{%- set seconds = int(secs % 60) -%}
Timer {{ state_attr(timer.entity_id, "friendly_name") }} has {{ hours }} Hours {{ minutes }} minutes and {{ seconds }} seconds Remaining, {{ "\n" }}
{%- endif -%}
{%- endfor %}
mode: single
alias: Time Left On Timers
icon: mdi:timer-settings-outline


All that just to be able to tell the thing to set a timer and have it behave appropriately. Still, I'm glad it's fine, and I learned a fair few things about the system and how to talk to it correctly in the process.
Depth: 1

Date: 2022-03-20 07:06 pm (UTC)
alexseanchai: Katsuki Yuuri wearing a blue jacket and his glasses and holding a poodle, in front of the asexual pride flag with a rainbow heart inset. (Default)
From: [personal profile] alexseanchai
πŸŽ‰πŸŽΊπŸ₯³
Depth: 1

Date: 2022-03-24 02:47 pm (UTC)
vass: Small turtle with green leaf in its mouth (Default)
From: [personal profile] vass
I love how you're documenting your process here.

Profile

silveradept: A kodama with a trombone. The trombone is playing music, even though it is held in a rest position (Default)
Silver Adept

July 2025

S M T W T F S
  1 2345
6789101112
13141516171819
20212223242526
2728293031  

Most Popular Tags

Page Summary

Style Credit

Expand Cut Tags

No cut tags
Page generated Jul. 10th, 2025 12:12 pm
Powered by Dreamwidth Studios