Posted by
ads' corner
on
Thursday, 2020-10-15 Posted in [Ansible][Openhab]
A while ago someone mentioned “reminders” used in their home audio system, and I took that idea and implemented something similar in openHAB.
The basic idea is that I can send scheduled notifications to any audio sink openHAB is using, possibly more than one audio sink for one message. Also I want to differentiate between a simple audio sound, and text output.
For the text audio output I installed Text-to-Speech a while ago, this enables the ability to output text as audio in different languages. In addition I want an information when a reminder is “fired” in my Telegram control channel. As audio sink I’m mostly using ChromeCasts here, but anything openHAB can connect to is usable here.
The implementation is split into two parts:
A YAML configuration file, which is included in Ansible
A rule file which is deployed as template using Ansible
The openHAB DSL is kind of limited for what I want to do with it, therefore I’m using Ansible to deploy everything, and along the way expand loops and such into functional code. Using Ansible has another upside for me: the entire openHAB setup is created using Ansible, at any point I can just reinstall everything, and don’t need to rely on backups. Did that once shortly after I started using openHAB, when the SDcard was dead because of the many writes (see here), and reinstalled everything a second time when I moved from a Rasperry 3 to a 4. That said, I have backups ;-) but I don’t really need it.
The configuration file
This file creates a single variable for Ansible, holding a list of dictionaries.
action: either say (for Text-to-Speech) or sound for playing an audio file
volume: the volume which the audio sink should be using
message: only used when action=say, the text for the TTS system
audio: only used when action=sound, the audio file to be played, must exist on the openHAB system in the audio folder
language: the TTS language
sink: list with audio sinks (I have the ChromeCasts in a YAML definition as well, and can address them by name and don’t need to use the ID)
The rules source file
By separating configuration and code I don’t need to touch the actual rules file when changing the schedule. I however have to re-deploy the configuration when the schedule changes. Maybe a future version can improve this.
First I need to include the configuration file in Ansible:
{%for r in reminders %}
rule "Reminder: {{ r['name'] }}"
when
Time cron "{{ r['time'] }}"
then
{%if r['enabled']== 1 %}
logInfo("Reminder","{{ r['name'] }}")
val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
telegramAction.sendTelegram(Long::parseLong(TELEGRAM_CHANNEL_HA_DETAILS),"Reminder: %s","{{ r['name'] }}"){%for as in r['sink']%}// loop over all audio sinks (ChromeCasts) and find the one specified by name
{% set ns.as_found= 0 %}{% set ns.as_used=''%}{% set ns.as_used_us=''%}{%for cc in chromecasts %}{%if cc['name']== as %}{% set ns.as_found= 1 %}{% set ns.as_used= cc['fullid']%}{% set ns.as_used_us= cc['fullid']|replace(':','_')%}{% endif %}{% endfor %}
logInfo("Reminder Debug","found: {{ ns.as_found }}"){{(''if ns.as_found== 1)| mandatory('ChromeCast ('+ as +') not found!')}}
logInfo("Reminder","Play on: {{ ns.as_used }}")if({{ ns.as_used_us}}_volume.state!={{ r['volume']}}){{{ ns.as_used_us}}_volume.sendCommand({{ r['volume']}})}{%if r['action']=="say"%}
playSound("{{ ns.as_used }}","1-second-of-silence.mp3")
Thread::sleep(1100)
playSound("{{ ns.as_used }}","computerbeep_69.mp3")
Thread::sleep(800)
say("{{ r['message'] }}","{{ r['language'] }}","{{ ns.as_used }}"){%else%}
playSound("{{ ns.as_used }}","1-second-of-silence.mp3")
Thread::sleep(1100)
playSound("{{ ns.as_used }}","{{ r['audio'] }}"){% endif %}{% endfor %}{%else%}// post a note that the reminder "fired", but is disabled
logInfo("Reminder","{{ r['name'] }} (disabled)"){% endif %}
end
{% endfor %}
And in detail:
Loop over all events, use only the ones which are enabled. Output a log note if an evert is disabled:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% for r in reminders %}rule "Reminder: {{ r['name'] }}"whenTime cron "{{ r['time'] }}"then{% if r['enabled'] == 1 %}logInfo("Reminder", "{{ r['name'] }}"){% else %}// post a note that the reminder "fired", but is disabledlogInfo("Reminder", "{{ r['name'] }} (disabled)"){% endif %}end{% endfor %}
Send a Telegram message:
1
2
val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
telegramAction.sendTelegram(Long::parseLong(TELEGRAM_CHANNEL_HA_DETAILS),"Reminder: %s","{{ r['name'] }}")
Loop over the list of audio sinks, and identify each. This will repeat the actual output code for each audio sink:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{%for as in r['sink']%}// loop over all audio sinks (ChromeCasts) and find the one specified by name
{% set ns.as_found= 0 %}{% set ns.as_used=''%}{% set ns.as_used_us=''%}{%for cc in chromecasts %}{%if cc['name']== as %}{% set ns.as_found= 1 %}{% set ns.as_used= cc['fullid']%}{% set ns.as_used_us= cc['fullid']|replace(':','_')%}{% endif %}{% endfor %}
logInfo("Reminder Debug","found: {{ ns.as_found }}"){{(''if ns.as_found== 1)| mandatory('ChromeCast ('+ as +') not found!')}}
logInfo("Reminder","Play on: {{ ns.as_used }}"){% endfor %}
The part for identifying the audio sink is a bit more complicated, because I want to use the plain name in the configuration, and need to loop over the list of ChromeCasts ($chromecasts) to find the actual entry. And bail out if a non-existent audio sink is specified. The error is already raised when the template is deployed by Ansible, which helps identifying any mistakes early on.
Next is setting the volume, but only if the volume setting is different from what is specified:
The ChromeCasts notoriously absorb the first bit of audio played on them, therefore the first thing played is a 1 second audio file with just silence as content. This will activate the ChromeCast, and smooth the next steps. The computerbeep_69.mp3 is just a nice touch, announcing that next comes amessage. If you ever watched Star Trek you know what I mean. And finally, the say() command outputs the actual message on the audio sink.
rule "Reminder: Get ready for school"
when
Time cron "0 0 7 ? * MON,TUE,WED,THU,FRI *"
then
logInfo("Reminder","Get ready for school")
val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
telegramAction.sendTelegram(Long::parseLong(TELEGRAM_CHANNEL_HA_DETAILS),"Reminder: %s","Get ready for school")// loop over all audio sinks (ChromeCasts) and find the one specified by name
logInfo("Reminder Debug","found: 1")
logInfo("Reminder","Play on: chromecast:audio:3105de263085d3df8f9021728582a2da")if(chromecast_audio_3105de263085d3df8f9021728582a2da_volume.state!= 50){
chromecast_audio_3105de263085d3df8f9021728582a2da_volume.sendCommand(50)}
playSound("chromecast:audio:3105de263085d3df8f9021728582a2da","1-second-of-silence.mp3")
Thread::sleep(1100)
playSound("chromecast:audio:3105de263085d3df8f9021728582a2da","computerbeep_69.mp3")
Thread::sleep(800)
say("Get ready for school","picotts:enUS","chromecast:audio:3105de263085d3df8f9021728582a2da")
end