433 Mhz-Funksteckdose in Amazon Alexa-Routine schalten
Es könnte ja eigentlich so einfach sein, aber ist es nicht: Mehrere Geräte zusammen ausschalten, z. B. wenn man ins Bett geht: Fernseher und Wohnzimmerlicht aus und Schlafzimmerlicht an."Könnte" weil: Alexa kennt Routinen, in denen man so etwas zusammenfassen kann. Aber eben auch nur, wenn es sich um WLAN-Geräte handelt.
Einen beliebigen oder eigenen Skill in einer Routine aufzurufen ist nicht möglich. Welch eine Schande! Es sind eine ganze Reihe von Aktionen möglich, aber die wichtigste - die, die universell Probleme lösen würde - fehlt natürlich: Man kann keine Sprachbefehle simulieren / ausführen in Routinen. Ja, nicht mal die Reihenfolge der Aktionen lässt sich ändern. Möchte man eine neue Aktion irgendwo einschieben, muss man alle nachfolgenden löschen, die neue Aktion anlegen und die alten dann danach anlegen. In meinen Augen ist das grausame Usability und grenzt an Folter. Die Android-Alexa-App gibt auch insgesamt ein schwaches und unfertiges Bild ab.
Wir können also unser Schlafzimmerlicht nicht einschalten, weil das über eine 433 Mhz- Funksteckdose über unseren eigenen Raspi-Skill geschaltet wird.
Die Sprachbefehle, die wir zusammenfassen wollen, sind eigentlich:
- Alexa, schalte Fernseher aus
- Alexa, schalte Wohnzimmerlicht aus
- Alexa, sage Display schalte Schlafzimmerlicht ein
- abschliessend soll Alexa noch eine gute Nacht wünschen
Also braucht es einen Workaround, um das auszumerzen, was die Alexa App sträflicherweise nicht selbst kann.
1. Lösungvariante: Alexa über Raspi-Script fernsteuern
Alexa, sage DISPLAY Gute Nacht!Wir haben ja bereits unser eigenen Display-Skill programmiert. Wenn wir den Gute-Nacht Befehl mit "Alexa, sage Display Gute Nacht" dort hinschicken, können wir die 433 Mhz- Steckdose schalten und müssten danach irgendwie noch die weiteren Alexa-Befehle ausführen.
In diesem Blog von Lötzimmer.de hat der Blog-Autor schon das Bedürfnis gehabt, Alexa von der Kommandozeile aus fernzusteutern und ein Script geschrieben, das unter anderem ermöglicht, in der Alexa-App definierte Routinen aufzurufen.
Die Lösung ist also, per Display-Skill unseren Raspi aufzurufen, dort den nötigen 433-Mhz-Befehl zu senden und dann dort das Script aufzurufen, dass sich dann wieder mit dem Echo verbindet und dort die Gute-Nacht-Routine aufruft und den Rest erledigt, der dort zu erledigen möglich ist.
Zuerst holen wir uns, nachdem wir die Funktionsweise auf der Blogseite studiert haben, das entsprechende script auf unseren Raspi und machen es ausführbar.
cd ~
wget https://loetzimmer.de/patches/alexa_remote_control.sh
chmod 777 alexa_remote_control.sh
Danach müssen wir in dem Script noch unsere e-mail-Adresse und Passwort bei amazon eintragen, damit die Verbindung zu unserem Echo hergestellt werden kann.Zum Parsen der JSON-Elemente in den Responses (kennen wir ja schon aus dem Blogbeitrag, in dem wir unseren eigenen Skill programmierten) müssen wir ggf. noch JQ installieren, falls noch nicht vorhanden:
sudo apt-get install jq
Danach können wir unseren Echo fernsteuern und die in der Alexa-App auf dem Smartphone angelegte Routine aufrufen. Da kommt wie gesagt alles rein, was Alexa selbst ausführen kann (siehe Screenshot oben):
~/alexa_remote_control.sh -d 'Olivers Echo Dot' -e automation:'gute Nacht'
Nachdem dieser kleine Test funktioniert hat (natürlich den eigenen Gerätenamen nehmen), schreiben wir uns ein zweites Script ...alexa_gute_nacht.sh
/home/pi/alexa_remote_control.sh -d 'Olivers Echo Dot' -e automation:'gute Nacht'
/home/pi/433Utils/RPi_utils/send 11111 2 1
... das wir dann nur noch in unserer alexa-skill.php aufrufen müssen, wenn wir den entsprechenden Skill-Befehl 'Gute Nacht' bekommen.Natürlich müssen wir noch die entsprechenden Erweiterungen an unserem Skill auf Amazon-Seite vornehmen, nämlich den Intent 'gutenacht' hinzufügen und ein paar Utterances angeben wie z. B. "gute nacht", "schlafenszeit" oder "bis morgen", damit der Sprachbefehl auch an unseren Raspi weitergeleitet wird. Danach das Model speichern und neu bauen nicht vergessen.
Unsere alexa-skill.php auf dem Raspi erweitern wir um ein paar Zeilen, um den neuen Intent abzufangen und das Script aufzurufen. Zum Schluss noch alles testen, ob es auch funktioniert. Bei eventuellen Fehler wegen Zugriffsverletzungen müssen wir ggf. den User www-data (den der Webserver und damit PHP benutzt) für die Scripte mit chgrp freischalten oder wir benutzen gleich usermod, um den User www-data permanent der Gruppe pi hinzuzufügen.
pi@raspberrypi:~ $ sudo -u www-data /home/pi/alexa_gute_nacht.sh
sudo: unable to execute /home/pi/alexa_gute_nacht.sh: Permission denied
pi@raspberrypi:~ $ sudo usermod -G pi -a www-data
Schlussendlich haben wir bekommen, was wir wollten, auch wenn die Frage "Warum einfach, wenn es auch kompliziert geht?" hier durchaus berechtigt ist.2. Lösungsvariante: Mit dem Raspi Geräte simulieren
Alexa, Gute Nacht!Ein weiterer Lösungsweg ist, mit unserem Raspi (der ja wegen des Always-On-Display eh ständig läuft) dem Echo virtuelle Steckdosen vorzugaukeln.
Das Projekt Fauxmo realisiert dies. Wenn Alexa neue Geräte sucht, fragt sie per WLAN in die Runde, wer ein Gerät ist, und was es kann. Fauxmo meldet sich dann - ggf. mehrfach - für alle virutellen Geräte, die dort definiert sind und gibt sich jeweils als Funksteckdose der Marke Belkin WeMo aus. Diese wird von Alexa unterstützt.
Alexa nimmt fortan an, dies seien echte Geräte und schickt dann Befehle an die IP unseres Raspberry Pis. Dieser fängt sie ab, führt die unter dem jeweiligen Gerät definierten Scripte aus und meldet Alexa den Schaltzustand zurück ("on" oder "off"). Entspricht der zurück gemeldete Schaltzustand dem gewünschten Ergebnis, sagt Alexa "okay". Falls nicht, gibt Alexa "Das Gerät reagiert gerade nicht" aus.
In den frei definierbaren Scriptaufrufen in Fauxmo rufen wir dann unsere 433Utils auf und senden den entsprechenden Schaltcode zum ein- nzw. ausschalten unserer 433-Mhz- Funksteckdose.
Danach taucht die Steckdose in der Alexa-App auf, kann darüber geschaltet werden oder per Sprachbefehl (etwa "Alexa, schalte Schlafzimmerlicht an") und verhält sich Alexa gegenüber wie eine echte WLAN-Steckdose. Auch wenn es in Wahrheit nur eine billige 433-MHz-Funksteckdose ist.
Fauxmo braucht in der derzeitigen Version mindestens Version 3.6 von Python. Per apt-get war bei mir nur max. Version 3.5.3-1 abrufbar. Darum muss zuerst Python 3.6 installiert werden.
Bevor wir Python 3.6.5 installieren, holen wir uns aber noch ein paar von Python 3.6 unterstützte Tools, damit die entsprechenden Python-Libraries in einem Abwasch gleich mit installiert werden. Eventuell werden wir sie noch an anderer Stelle brauchen. Wer Speicherplatzprobleme auf seinem Pi hat, kann diesen Schritt überspringen.
sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev
Danach installieren wir Python 3.7 mittel pyenv. Pyenv ist eine Art Versions-Management, um mehrere Python Versionen parallel nutzen zu können. So bleibt die alte Python-Version für unsere alten Scripte vorhanden.sudo install -o $(whoami) -g $(whoami) -d /opt/pyenv
git clone https://github.com/yyuu/pyenv /opt/pyenv
echo 'export PYENV_ROOT="/opt/pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
source ~/.bashrc
pyenv install 3.7.0
Auf meinen Raspi Zero hat die Installation eine Ewigkeit gedauert. Die Zeit reicht locker für einen ausgiebigen Spaziergang. Also nicht abbrechen, sondern Geduld zeigen.Danach holen wir uns noch python3-venv
sudo apt-get install python3-venv
Und schließlich auch Fauxmo, dass wir gleich installieren
git clone https://github.com/n8henrie/fauxmo.git
cd fauxmo
python3 -m venv .venv
source ./.venv/bin/activate
pip install -e .[dev]
Nun brauchen wir noch eine Konfigurationsdatei, in der definiert wird, was Faxumo bei welchem ankommenden Alexa-Befehl tun soll. Hier können wir schon einmal einen Blick hineinwerfen, auch wenn unsere config zum Schluss ganz anders aussehen wird.
cp config-sample.json config.json
nano config.json
Denn wir brauchen ein Plugin, das Befehle auf der Kommandozeile ausführen kann und keins, das Webseiten aufruft, so wie das SimpleHTTPPlugin aus der Beispiel-Config.Unter https://github.com/n8henrie/fauxmo-plugins/blob/master/commandlineplugin.py findet man ein solches Plugin.
Ich fand es allerdings ein wenig umständlich bezüglich der Generation des richtigen Rückgabewertes an Alexa. Darum habe ich es etwas umgeschrieben, damit es den Einschaltzustand eines Gerätes selbst verwaltet und zurückmeldet. Hier ist der Source-Code, den wir als fauxmo-cli-plugin.py in unserem Home-Verzeichnis speichern.
fauxmo-cli-plugin.py
"""Fauxmo plugin that runs a command on the local machine.
edit: Oliver Kuhlemann (cool-web.de), 2019-06-04: handle state by plugin, not by state_cmd
Runs a `shlex`ed command using `subprocess.run`, keeping the default of
`shell=False`. This is probaby frought with security concerns, which is why
this plugin is not included by default in `fauxmo.plugins`. By installing or
using it, you acknowledge that it could run commands from your config.json that
could lead to data compromise, corruption, loss, etc. Consider making your
config.json read-only. If there are parts of this you don't understand, you
should probably not use this plugin.
If the command runs with a return code of 0, Alexa should respond prompty
"Okay" or something that indicates it seems to have worked. If the command has
a return code of anything other than 0, Alexa stalls for several seconds and
subsequently reports that there was a problem (which should notify the user
that something didn't go as planned).
Note that `subprocess.run` as implemented in this plugin doesn't handle complex
commands with pipes, redirection, or multiple statements joined by `&&`, `||`,
`;`, etc., so you can't just use e.g. `"command that sometimes fails || true"`
to avoid the delay and Alexa's response. If you really want to handle more
complex commands, consider using this plugin as a template for another one
using `os.system` instead of `subprocess.run`, but realize that this comes with
substantial security risks that exceed my ability to explain.
Example config:
```
{
"FAUXMO": {
"ip_address": "auto"
},
"PLUGINS": {
"CommandLinePlugin": {
"path": "/path/to/commandlineplugin.py",
"DEVICES": [
{
"name": "output stuff to a file",
"port": 49915,
"on_cmd": "touch testfile.txt",
"off_cmd": "rm testfile.txt"
}
]
}
}
}
```
"""
import shlex
import subprocess
from fauxmo.plugins import FauxmoPlugin
class CommandLinePlugin(FauxmoPlugin):
"""Fauxmo Plugin for running commands on the local machine."""
SwitchState = "unknown"
def __init__(self, name: str, port: int, on_cmd: str, off_cmd: str) -> None:
"""Initialize a CommandLinePlugin instance.
Args:
name: Name for this Fauxmo device
port: Port on which to run a specific CommandLinePlugin instance
on_cmd: Command to be called when turning device on
off_cmd: Command to be called when turning device off
state_cmd: Command to check device state (return code 0 == on)
"""
self.on_cmd = on_cmd
self.off_cmd = off_cmd
super().__init__(name=name, port=port)
def run_cmd(self, cmd: str) -> bool:
"""Partialmethod to run command.
Args:
cmd: Command to be run
Returns:
True if command seems to have run without error
"""
shlexed_cmd = shlex.split(cmd)
process = subprocess.run(shlexed_cmd)
return process.returncode == 0
def on(self) -> bool:
"""Run on command.
Returns:
True if command seems to have run without error.
"""
self.run_cmd(self.on_cmd)
self.SwitchState = "on"
return True;
def off(self) -> bool:
"""Run off command.
Returns:
True if command seems to have run without error.
"""
self.run_cmd(self.off_cmd)
self.SwitchState = "off"
return True;
def get_state(self) -> str:
"""Get device state.
NB: Return code of `0` (i.e. ran without error) indicates "on" state,
otherwise will be off. making it easier to have something like `ls
path/to/pidfile` suggest `on`. Many command line switches may not
actually have a "state" per se (just an arbitary command you want to
run), in which case you could just put "false" as the command, which
should always return "off".
Returns:
"on" or "off" if `state_cmd` is defined, "unknown" if undefined
"""
print ("State of " + self.name + " is " + self.SwitchState + ".")
return self.SwitchState
Wie wir erkennen können gibt es vier Attribute, die pro Gerät zu definieren sind:
- name: Der Name, der in der Alexa-App erscheint un den wir für Alexa-Sprachkommandos benutzen, etwa "Schlafzimmerlicht"
- port: Ein freier Port im oberen Bereich (fünfstellig, max. 65535) zur Kommunikation mit Alexa
- on_cmd: Das Script, das ausgeführt wird, wenn die virtuelle Steckdose durch Alexa eingeschaltet wird
- off_cmd: ... und wenn die Steckdose ausgeschaltet wird
config.jason
{
"FAUXMO": {
"ip_address": "auto"
},
"PLUGINS": {
"CommandLinePlugin": {
"path": "/home/pi/fauxmo-cli-plugin.py",
"DEVICES": [
{
"name": "Schlafzimmerlicht",
"port": 12345,
"on_cmd": "/home/pi/433Utils/RPi_utils/send 11111 2 1",
"off_cmd": "/home/pi/433Utils/RPi_utils/send 11111 2 0"
}
]
}
}
}
Danach starten wir Fauxmo einfach mit
pi@raspberrypi:~/fauxmo $ fauxmo
Wenn alles richtig konfiguriert ist, läuft Fauxmo nun und beantwortet Alexa-Anfragen.Lassen wir Alexa mit "Alexa, suche nach neuen Geräten" nach den Fauxmo-Geräten suchen.
Sie sollte dann unter "Geräte" so etwas wie "x Geräte gefunden anzeigen". Unter Steckdosen finden wir dann unsere neuen Geräte.
Nun kann man das AN bzw. AUS Feld antippen, um die Steckdose zu schalten. Alternativ funktioniert auch der Sprachbefehl "Alexa, schalte Schlafzimmerlicht an bzw. aus".
Gleichzeitig gibt Fauxmo entsprechende Meldungen aus:
pi@raspberrypi:~/fauxmo $ fauxmo
sending systemCode[11111] unitCode[01000] command[0]
State of Schlafzimmerlicht is off.
Da die Steckdose jetzt endlich als Alexa-Gerät vorhanden ist, kann sie auch in die Routine "Gute Nacht" mit aufgenommen werden.
Fauxmo sollten wir abschließend so einrichten, dass es sich automatisch mit dem System startet:
sudo nano /etc/.rclocal
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
# Print the IP address
#_IP=$(hostname -I) || true
#if [ "$_IP" ]; then
# printf "My IP address is %s\n" "$_IP"
#fi
python /home/pi/monitor.py &
/opt/pyenv/versions/3.7.0/bin/python3.7 -m fauxmo.cli -c /home/pi/fauxmo/config.json &
exit 0
Stellenweise wird empfohlen, Fauxmo über die crontab zu starten. Das hat bei mir aber nie geklappt. Die Methode über rc.local funktioniert bei mir hingegen reibungslos.Diese Lösungs-Methode ist vielleicht noch ein bisschen komfortabler in der Bedienung als die Erste, weil hier nur "Alexa, Schlafzimmerlicht an" statt "Alexa, sage Display schalte Schlafzimmerlicht an" gesagt werden muss. Bzw. "Alexa, gute Nacht" statt "Alexa, sage Display gute Nacht". Außerdem tauchen die Steckdosen in der Alexa-App auf und sind dort per Smartphone schaltbar.
Abwägung Nutzen / Aufwand
"So viel Aufwand, nur weil die Alexa-App zu doof ist, Skills in Routinen aufzurufen?" könnte man jetzt fragen. Ja, die Workarounds sind nicht ganz unkompliziert und es dauert sicher eine Weile, bis sie eingerichtet sind. Normalerweise würde ich sagen, es lohnt den Aufwand nicht, nur um abends zur Bettgehzeit einen statt drei Sätze zu sagen. Aber wenn man technisch interessiert ist so wie ich, und es als Herausforderung sieht, dann kann man das schon mal machen.Da die zweite Lösung bis zu 16 Geräte bereitstellen kann, habe ich außerdem für die Zukunft vorgesorgt, wenn ich statt teurer WLAN-Steckdosen billige 433Mhz- Funksteckdosen benutzen will. Hier muss ich jeweils nur den Eintrag in der config.json von Fauxmo kopieren und editieren und kann die Steckdose dann ohne großen weiteren Aufwand mit Alexa fernsteuern.