timer- und pups-intents für homeassistant und snips

felix schwenzel, , in artikel    

alexa, bzw. unser echo-dot ist jetzt seit ungefähr 6 wochen offline und empfängt jetzt ihr gnadenbrot. wenn ich artikel über neue oder nützliche features von alexa lese zucke ich meist mit der schulter und bemerke, dass mich die meisten skills oder features von alexa ohnehin nicht interessiert haben und dass es gerademal 5 sachen gab, die wir/ich regelmässig an alexa herangetragen haben:

  • licht- und gerätesteueerung, vor allem in der küche, wo alexa lebte uns zuhörte
  • timer
  • füllen unserer gemeinsamen einkaufsliste in bring per zuruf
  • gelegentliche fragen nach öffnungszeiten oder wikipedia-artikeln

die licht und gerätesteuerung macht snips mittlerweile, wie ich finde, besser als alexa, vor allem auch, weil ich den lampen, geräten und räumen einfacher (un beliebig viele) synonyme geben kann und vor allem weil ich die aktionen nach gutdünken, vor allem kurz gefasst aufrufen kann. kann natürlich auch sein, dass mir das merken leichter fällt, weil ich mir die triggersätze ausgedacht habe und nicht ein amazon-mitarbeiter. es kann aber auch sein, dass ich die licht-, geräte- und raumzuordnungen per homeassistant besser und einfacher strukturieren konnte, als mit der alexa app. aber das ist ein anderer artikel. genauso werde ich einen artikel darüber schreiben, wie ich snips dazu gebracht habe einkaufserinnerungen in bring zu bekommen, obwohl bring sich weigert eine öfffentliche API anzubieten (spoiler: sie haben eine API, den alexa-skill). lediglich wissenfragen nach wikipedia-artikeln oder öffnungszeiten von geschäften in der nähe konnte ich snips noch nicht beibringen, vor allem weil ein snips ein allgemeines deutsches wörterbuch fehlt, snips also kurzgesagt nur das versteht, was man snips explizit beigebracht hat.

was ich bei alexa wirklich häufig genutzt habe war die timer-funktion. was mich allerdings immer gestört hat, war das fehlende visuelle feedback. um zu erfahren wie lange der timer noch läuft, musste ich immer nachfragen. das kann jede eieruhr besser. sämtliche versuche per API auf die alexa/echo timerfunktionen zuzugreifen scheiterten, alles was über die API (per IFTTT) möglich schien, war eine aktion nach dem ablaufen von timern zu triggern, was ich aber nicht brauchte.

die timer-funktion, die ich hier neben der ebenso wichtigen „pups mal!“-aktion abgefilmt habe, habe ich natürlich mit hilfe vom home-assistant gebaut. wie genau, erzähle ich im folgenden, muss dafür aber vorher nochmal kurz ausholen.

skills legt man mit snips in der (online) konsole von snips an (das bauen von assistenten und skills ist die einzige funktion von snips, die (noch) nicht offline verfügbar ist, nach dem deployment funktioniert snips dann aber zu 100% offline).

snips konsole

der „skill“ timer besteht aus zwei „intents“, timer starten und timer stoppen. die intents findet snips „schwach“, weil ich jeweils nur sechs, bzw. elf trainingssätze eingegeben habe.

snips timer app

die trainingssätze sind die sätze die snips erkennen soll, wenn ich einen timer starten möchte, also zum beispiel:

  • Erinner mich in 2 Minuten
  • Wecke mich in einer Stunde
  • Erinnere mich in 30 Minuten
  • Nudeltimer von 8 Minuten
  • Eieruhr für 6 Minuten
  • Wecker in 15 Sekunden
  • Timer 30 Sekunden
  • 2 Minuten Countdown
  • Starte einen Countdown für 2 Minuten
  • 10 Minuten Timer
  • Timer 10 Minuten

damit ich nicht je einen trainingssatz für jeden möglichen zeitraum aufschreiben muss, gibt es vorgefertigte slots für standardwerte wie die dauer, zahlen, temperaturen oder geldbeträge. deshalb habe ich dem intent einen slot für die timer-dauer hinzugefügt. die satzteile mit der dauer muss man anfangs selbst markieren, nach einer weile lernt die konsole dazu und erkennt die dauer in beispielsätzen alleine. der zweite slot erfasst den namen des timers, so dass ich theoretisch mehrere timer parallel aufsetzen kann oder snips mich beim beenden des timers daran erinnern kann, um was der timer geht.

snips intent TimerStart

im prinzip ist das schon alles was man für meine lösung auf snips-seite anlegen muss. den rest erledige ich mit homeassistant. man kann für snips auch aktionen in python programmieren, die auf den intent reagieren und agieren. diese python-scripte installiert snips dann auch lokal auf dem raspberry. es gibt ein paar fertige skills (oder apps) die man in einer art app-store in der konsole installieren kann. ein paar von denen habe ich ausprobiert, aber meistens waren die anpassungen die ich an diesen fertigen apps vornehmen musste oder wollte aufwändiger als es mit homeassistant selbst zu machen. die meisten der skills die ich in der konsole anlege haben deshalb gar keine aktionen.

snips timer app aktionen (keine)

trotzdem haben die skills, oder genauer die einzelnen intents aktionen zur folge, wenn man im homeassistant die snips-komponnete installiert hat. dann schnappt sich homeassistant sozusagen die intents auf, die man konfiguriert hat. für den TimerStart intent sieht das dann so aus:


intent_script:
  TimerStart:
    speech:
      text: >-
          timer {{timer_duration_raw}} ab jetzt.
          {% if timer_duration > 3600 %}
              Timer von mehr als einer Stunde stelle ich wahrscheinlich falsch dar.
              Felix ist da zu doof für, das ordentlich zu machen.
          {% endif %}
    action:
      - service: timer.finish
        entity_id: timer.snips
      - service: timer.start
        data_template:
          entity_id: timer.snips
          duration: '{{ timer_duration }}'

diese zeilen bitten homeassistant, sobald snips das auslösen des TimerStart-Intents meldet, aktiv zu werden. einerseits mit einem audio-feedback und andererseit mit einer aktion. der intent liefert nach dem auslösen die aufgeschnappten „slots“ mit, also in diesem fall die dauer (timer_duration) und den namen (timer_name). wie man sieht, ignoriere ich den namen und werte bis jetzt lediglich die dauer aus.

wenn ich also sage „10 minuten timer“ erkennt snips die dauer (10 minuten) und den intent (timer start) und gibt das auf dem „mqtt-bus“ bekannt. weil homeassistant den bus abhört arbeitet homeassistant dann meine konfiguration ab und weist snips folgendes an sprachfeedback zu geben: „timer 10 minuten ab jetzt.“ ausserdem stoppt homeassistant eventuell schon laufende timer und startet einen neuen timer mit der übermittelten dauer (timer_duration wird von der homeassistant-snips-komponente freundlicherweise in sekunden umgerechnet, timer_raw ist der der eingabe-, also der rohe wert).

weil jetzt ausser einem laufenden timer nichts weiter passieren würde, muss ich natürlich noch eine automation anlegen, die sich um die darstellung der restlaufzeit kümmert und eine, die den abgelaufenen timer ankündigt.


automation:
  - alias: timerstarted
    trigger:
      platform: time
      seconds: '/1'
    condition:
      condition: state
      entity_id: timer.snips
      state: active
    action:
      - service: counter.increment
        data:
          entity_id: counter.snips_elapsed
      - service: mqtt.publish
        data_template:
          topic: "devices/led-matrix/light/print/set"
          payload: >-
              {% set duration = states.timer.snips.attributes.duration %}
              {% set elapsed = states("counter.snips_elapsed") | default(0) | int %}
              {% set hms_split = duration.split(':') %}
              {% set hours = hms_split[0] | int %}
              {% set minutes = hms_split[1] | int %}
              {% set seconds = hms_split[2] | int %}
              {% set duration = seconds + (minutes * 60) + (hours * 60 * 60) %}
              {{ (duration - elapsed ) | int | timestamp_custom('%M%:%S') }}
  - alias: timerfinished
    trigger:
      platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.snips
    action:
      - service: counter.reset
        data:
          entity_id: counter.snips_elapsed
      - condition: state
        entity_id: timer.snips
        state: idle
      - service: mqtt.publish
        data_template:
          topic: "devices/led-matrix/light/print/set"
          payload: 'fertig!'
      - service: snips.say
        data_template:
          site_id: "ivanka"
          text: "dein timer ist abgelaufen"

die erste automation läuft jede sekunden wenn der timer.snips läuft (sonst nicht). die action berechnet, bzw. zählt die restlaufzeit (in sekunden) und schickt die restlaufzeit auf meinen selbstgebauten matrix-display, der sich per mqtt füttern lässt. das payload-template macht nichts anders als aus der timer-zeit und der abgelaufenen zeit die verbleibenden minuten und sekunden auszurechnen und sie im format '%M%:%S' darzustellen.

die zweite automation wird getriggert, sobald der timer abgelaufen ist und stellt den text „fertig“ auf dem led-matrix-bildschirm dar und lässt snips sagen: „dein timer ist abgelaufen“.

das ganze würde eventuell mit einem python-script viel einfacher umzusetzen zu sein, aber für komplexeres python bin ich noch zu doof. die homeassistant yaml-konfiguration ist auch nicht gerade trivial, aber weil ich mittlerweile in dieser form gefühlt 800 automatisierungen für die wohnung geschrieben habe, bin ich da relativ trittsicher.

das beispiel zeigt die qualität vom zusammenspiel von snips und homeassistant eigentlich ganz gut: im prinzip ist das alles recht einfach, aber man muss halt fast alles selbst machen. die snipskonsole und dokumentation helfen, homeassistant nimmt einem sowieso einen grossen teil arbeit ab, aber jeden einzelfall, jedes detail muss man selbst bedenken. die nüsse die man für einzelne skills knacken muss sind teils weich, teils sehr hart. mir hat das in den letzten wochen aber grossen spass bereitet diese nüsse einzeln zu knacken: wie bekomme ich snips/homeassistant dazu einzelne lichter, geräte oder lichtszenen zu schalten, wie kann ich meine bring-einkaufsliste per zuruf füllen, wie nach temperaturen fragen. das ist ein bisschen wie kreuzworträtsel lösen, mit dem unterschied, dass man sich die aufgaben und lösungen hier selbst ausdenken muss und es keine richtige oder falsche lösung gibt, sondern nur jeweils eine, die ausreichend gut funktioniert.

* * *

snips pupsen app und pupsen intent

wo ich gerade dabei bin erklär ich noch, wie ich snips (ivanka) das pupsen beigebracht habe. den intent, bzw. die trainingssätze anzulegen war eher trivial:

  • nochmal pupsen
  • bitte pups nochmal
  • pups nochmal
  • pupsgenerator
  • flatulenz
  • bitte flatulieren
  • flatuliere bitte
  • bitte furzen
  • bitte pupsen
  • pup mal
  • kannst du furzen?
  • bitte furz mal
  • bitte pups mal
  • furzen
  • furz mal
  • pupsen
  • pups mal
  • pupse bitte
  • kannst du pupsen?

jeder dieser sätze triggert im homeassistant den intent pupsen:


intent:
  pupsen:
    async_action: True
    action:
      - service: shell_command.snips_wav
        data_template:
          site_id: "{{ site_id }}"
          session_id: "{{ session_id }}"
          wav_file: >-
              {%- set file = [
              "Girl Fart-SoundBible.com-669012925",
              "Quick An Small Fart-SoundBible.com-958779407.wav",
              "Fart Reverberating Bathroom-SoundBible.com-2032114496.wav",
              "Wet Fart Squish-SoundBible.com-332766022.wav",
              "Fart Short Ripper-SoundBible.com-1317602707.wav",
              "King Farthur-SoundBible.com-922925717.wav",
              "Funny Fart Trail-SoundBible.com-1691782690.wav",
              "Squish Fart-SoundBible.com-115133916.wav",
              "Rigid Fart-SoundBible.com-1121832279.wav",
              "Quick Fart-SoundBible.com-655578646.wav",
              "Windy Fart-SoundBible.com-1980462064.wav",
              "Bean Fart-SoundBible.com-215806729.wav",
              "Silly_Farts-Joe-1473367952.wav"
              ] | random -%}
              /Users/ix/.homeassistant/sounds/pupse/{{ file }}
shell_command:
  snips_wav: >-
      /usr/local/Cellar/mosquitto/1.4.14_2/bin/mosquitto_pub -h ivanka.local -p 1883 -u <USER> -P <PASSWORD> -t 'hermes/audioServer/{{ site_id }}/playBytes/{{ session_id }}' -f '{{wav_file}}'

async_action sagt snips bescheid, dass der intent ausgeführt wurde, auch wenn die aktion noch nicht zuende ausgeführt wurde. die aktion besteht im prinzip aus einem kommandozeilen befehl der snips eine wav-datei zum abspielen schickt. die auswahl der wav-datei erfolgt per zufall aus 13 dateien die ich mir zusammengegooglet habe. wichtig sind noch die variablen site_id und session_id. die beiden werte liegen bei jedem intent-aufruf vor und sind dann wichtig, wenn man mehrer snips-assistenten zuhause hat, also in verschiedenen räumen. ich habe im kinderzimmer einen snips-satelitten installiert und wenn ich snips dort frage mal zu pupsen, wird die wav-datei eben auch dort abgespielt.

der kommandozeilenbefehl schickt nach dem aufruf durch den intent die wav-datei per mqtt an den snips-audio-server. durch die site_id weiss snips auf welchem lautsprecher die datei abzuspielen ist und das ist ungefähr alles was man tun muss, um snips zum pupsen aufzufordern.

weil ich es lustig fand pups geräusche im bad abzuspielen, wenn dort jemand anders sitzt, habe ich mnoch einen zusätzlichen skill zusammengestellt, der genau das macht. eleganter wäre es natürlich alles in einem skill, bzw. intent abzuhandeln, also zu prüfen, ob der intent einen raum mitliefert oder nicht und entsprechend zu agieren (bei keiner raumnennung abspielen im raum wo der intent getriggert wurde, sonst im genannten raum).

* * *

hier ist ein gist, mit der homeassistant konfiguration für den timer und den pups intent.