Motivation

Vor kurzem ist Alexa (Amazon Echo) bei mir eingezogen. Alexa kann verschiede Aufgaben erledigen, u.a. solche aus dem Bereich Hausautomation per WiFi. Es gibt eine Reihe von Projekten, die mit dem ESP8266 funktionieren. Sucht man danach, findet man schnell viele, die eine WiFi-Steckdose (Wemo Switch) vom Belkin emulieren. Dabei gibt es viel Spreu und Weizen. Das hat mich bewogen, mich intensiver mit der Technologie auseinander zu setzen: hier meine Version eines Wemo-Switch-Emulators. Grundlage ist das GitHub-Projekt multiple belkin wemos switch emulator using ESP8266 von Aruna Tennakoon.

Andere Varianten:

In­halts­ver­zeich­nis

Bibliothek UrsWemoEmu

Verwendung

Methodenübersicht

Hinweise zu Umlauten

Beispiel

Download

Funktionsweise

Alexa-Befehle

Datenfluss / Protokolle

Geräteerkennung

Geräte schalten

Implementierung

UrsWemoManager

UrsWemoSwitch


UrsWemoManager

Bibliothek UrsWemoEmu

Die Bibliothek besteht aus drei Klassen:

Verwendung

Zunächst müssen die Switch-Objekte erstellt werden. Diese können entweder statisch vorliegen (RedLight und BlueLight im Beispiel) und dann registriert werden ...

UrsWemoSwitch BlueLight("blaues Licht", 82, BlueLightPin);     // An D5 angeschlossene blaue LED
UrsWemoSwitch RedLight ("rotes Licht",  81, RedLightPin, LOW); // Interne LED des NodeMCU
...
setup() {
...
  UrsWemoManager.addDevice(RedLight);
  UrsWemoManager.addDevice(BlueLight);
...

... oder direkt bei der Registrierung auf dem Heap erzeugt werden

UrsWemoManager.addDevice(*(new UrsCallbackWemoSwitch("Beleuchtung", 83, LightSwitch)));

Die Methode begin() startet wie üblich die zu Grunde liegen UDP- und WebServer

UrsWemoManager.begin();

Dann muss der gesamten Konstruktion durch Aufruf der Methode handle() Rechenzeit zugeteilt werden:

void loop() {
  ...
  UrsWemoManager.handle();
  ...
  delay(0);
}

UrsWemoManger steht als statisches Objekt zur Verfügung und braucht nicht instanziiert zu werden.

Methodenübersicht

UrsWemoManager

Funktion Beschreibung Anmerkung
void addDevice(UrsWemoSwitch & device) Registriert einen Switch beim UrsWemoManager.  
bool begin() Startet den Manager. Erst aufrufen, nachdem alle Switches registriert wurden!
void handle() Stellt den Emulator-Service bereit. In loop() aufrufen.
void setOutdevice(Print& od) Legt ein neues Ausgabe-Gerät für Protokollmeldungen fest. Standard ist Serial.
Beispiel:
UrsWemoManager.setOutdevice(UrsDummyPrint) unterdrückt die Ausgabe.
void setErrdevice(Print& od) Legt ein neues Ausgabe-Gerät für Fehlermeldungen fest. Standard ist Serial.
Beispiel:
UrsWemoManager.setErrdevice(UrsDummyPrint) unterdrückt die Ausgabe.

UrsWemoSwitch

Funktion Beschreibung Anmerkung
UrsWemoSwitch(
    String alexaInvokeName,
    unsigned int WebServerPort,
    uint8_t digitalPin,
    uint8_t highValue = HIGH)

Konstruktor:

  • alexaInvokeName Alexa-Aufrufname
  • webServerPort Port des Webservers zur Kommunikation mit Alexa
  • digitalPin Nurmmer des Pins, der geschaltet werden soll
  • highValue Wert, der bei der Anweisung "Anschalten" an den Pin weiter gegeben werden soll (HIGH oder LOW).
AlexaInvokeName und der WebServer-Port müssen eindeutig sein!
String getAlexaInvokeName() Ruft den Alexa-Aufrufname des Switches ab.  
virtual void onSwitchOn()
Standardfunktion zum Aktivieren des eingestellten Pins.  
virtual void onSwitchOff() Standardfunktion zum Deaktivieren des eingestellten. Pins  

UrsCallbackWemoSwitch

Funktion Beschreibung Anmerkung
UrsCallbackWemo (
    String alexaInvokeName,
    unsigned int webServerPort,
    WemoSwitchCallback callback)

Konstruktor:

  • alexaInvokeName Alexa-Aufrufname
  • webServerPort Port des Webservers zur Kommunikation mit Alexa
  • callbac Methode vom Typ void func(bool)
AlexaInvokeName und der WebServer-Port müssen eindeutig sein!

Der boolesche Wert der Callback-Funktion ist True für die Anweisung "Anschalten" und False für "Abschalten".
String getAlexaInvokeName() Ruft den Alexa-Aufrufname des Switches ab.  
virtual void onSwitchOn()
Standardfunktion zur Aktivierung. Ruft die Callback-Funktion mit booleschem Parameter True auf.
virtual void onSwitchOff() Standardfunktion zum Deaktivieren des eingestellten. Pins Ruft die Callback-Funktion mit booleschem Parameter False auf.

Hinweise zu Umlauten

Umlaute im Alexa-Aufrufnamen können nicht direkt eingefügt werden. Es müssen Unicode-Ersatzdarstellungen genommen werden, z.B. "\u00fc" für "ü". Aus "Küche" wird dann "K\u00fcche".

Umlaut Ersatz
Ä \u00c4
ä \u00e4
Ö \u00d6
ö \u00f6
Ü \u00dc
ü \u00fc
ß \u00df

Beispiel

Das Beispiel richtet drei Geräte ein:

#include <ESP8266WiFi.h>
#include <UrsWiFi.h>
#include <UrsWemoSwitch.h>
#include <UrsCallbackWemoSwitch.h>
#include <UrsWemoManager.h>

// Netzwerkzugangsdaten anpassen!!!!!
const char* ssid = "xxxxxx";
const char* password = "xxxxx";
boolean wifiConnected = false;

const int BlueLightPin = D5; // An D5 angeschlossene blaue LED
const int RedLightPin  = D0; // Interne LED des NodeMCU

UrsWemoSwitch BlueLight("blaues Licht", 82, BlueLightPin); // An D5 angeschlossene blaue LED
UrsWemoSwitch RedLight ("rotes Licht",  81, RedLightPin, LOW); // Interne LED des NodeMCU

void LightSwitch(bool);  // Callback

void setup() {
  Serial.begin(115200);


  // WiFi-Verbindung herstellen
  wifiConnected = (UrsWiFi.connectStation(ssid, password, "UrsWemo") == 0); // siehe UrsWiFi

  if (wifiConnected) {
    UrsWemoManager.addDevice(RedLight);
    UrsWemoManager.addDevice(BlueLight);
    UrsWemoManager.addDevice(*(new UrsCallbackWemoSwitch("Beleuchtung", 83, LightSwitch)));
    UrsWemoManager.begin();

  }
  else {
    Serial.println("Rebooting"); // Ohne Netzwerkverbindung geht nichts
    ESP.restart();
  } // if wifiConnected
} // setup

void loop() {
  UrsWemoManager.handle();
  delay(0);
}

// Callback für 'Beleuchtung'
void  LightSwitch(bool onoff) {
  if (onoff) {
    Serial.println("'Beleuchtung' turn on ...");
    RedLight.onSwitchOn();
    BlueLight.onSwitchOn();
  }
  else {
    Serial.println("'Beleuchtung' turn off ...");
    RedLight.onSwitchOff();
    BlueLight.onSwitchOff();
  }
}

Download

Das ZIP-Archiv für Bibliothek UrsWemoEmu zum Download. Die entpackten Dateien ins Verzeichnis <user>\Documents\Arduino\libraries kopieren (siehe Installing Additional Arduino Libraries).

Das Archiv enthält die Bibliotheksdateien

Funktionsweise

Alexa-Befehle

Alexa kennt drei Befehle zur Steuerung der Wemo-Switches:

Dabei sind unterschiedliche Formulierungen möglich. Alexa reagiert z.B auf "An" genauso wie auf "Anschalten".

Datenfluss / Protokolle

Die prinzipielle Funktionsweise zeigt die folgende Grafik:

Echo Datenfluss

Geräteerkennung

Der aufwendigste Teil ist die Geräte-Erkennung. Sie erfolgt per UPnP (Universal Plug and Play).

Links zu UPnP:

Nachdem Alexa den Auftrag "Suche neue Geräte" bekommen hat, sendet es per UPD einen "M-SEARCH"-Request an die Multicast-Adresse 239.255.255.250:1900. Es wird nach "urn:Belkin:device:**" gesucht.

M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 15
ST: urn:Belkin:device:**

Die Geräte, die mit Alexa kommunizieren wollen, reagieren auf diesen Request, in dem sie an den Absender per HTTP via UDP die URL zurückmelden, über die Setup-Information erhältlich sind (LOCATION, rot markiert), und die eindeutige ID des Geräts (USN, rot markiert). Es können mehrere Geräte auf den M-SEARCH-Request antworten (Multicast!).

HHTTP/1.1 200 OK
CACHE-CONTROL: max-age=86400
LOCATION: http://192.168.178.31:81/setup.xml
SERVER: Unspecified, UPnP/1.0, Unspecified
ST: urn:Belkin:device:**
USN: uuid:Socket-1_0-38323636-4558-4dda-9188-cda0e6d43948-81::urn:Belkin:device:**

Bei den zu Beginn aufgeführten Projekten enthält die Antwort z.T. weitere Komponenten, die aber nicht unbedingt notwendig sind.

Als nächstes ruft Alexa die Setup-Daten über die oben übermittelte URL ab (http://192.168.178.31:81/setup.xml). Die Antwort ist:

<?xml version="1.0"?>
<root>
  <device>
    <deviceType>urn:Ulli:device:controllee:1</deviceType>
    <friendlyName>Wohnzimmerlicht</friendlyName>
    <manufacturer>Belkin International Inc.</manufacturer>
    <modelName>Ullis Emulated Socket</modelName>
    <modelNumber>3.21415</modelNumber>
    <UDN>uuid:Socket-1_0-38323636-4558-4dda-9188-cda0e6d43948-82</UDN>
    <serialNumber>Ulli1</serialNumber>
    <binaryState>0</binaryState>
    <serviceList>
      <service>
        <serviceType>urn:Belkin:service:basicevent:1</serviceType>
        <serviceId>urn:Belkin:serviceId:basicevent1</serviceId>
        <controlURL>/upnp/control/basicevent1</controlURL>
        <eventSubURL>/upnp/event/basicevent1</eventSubURL>
        <SCPDURL>/eventservice.xml</SCPDURL>
      </service>
    </serviceList>
  </device>
</root>

Die rot markierten Komponenten sind beliebig und können durch eigene ersetzt werden. In dem blau markierten Abschnitt ("friendlyName") wird der Name übergeben, der Alexa zur Benennung dieses Geräts dient.

Die Suche nach neuen Geräten muss i.d.R. nur einmal erfolgen. Alexa speichert die Verbindungsdaten. Sofern das gefundene Gerät seine Adressdaten nicht ändert (z.B. die IP oder den Port, über den das Gerät erreichbar ist), verwendet Alexa die gespeicherten Informationen. Dies klappt sowohl nach Ein- und Ausschalten von Alexa als auch des Wemo-Emulators. Je nach Netzwerkeinstellung ist es jedoch möglich, dass das Gerät bei erneutem Einschalten eine neue IP erhält. In diesem Fall ist eine neue Gerätesuche unumgänglich.

Geräte schalten

Hat Alexa die Geräte registriert, lassen sie sich per gesprochenem Befehl an- und abschalten. Alexa sendet hierzu den nachfolgenden Request als (Query-String) an die registrierte URL.

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
      <BinaryState>0</BinaryState>
    </u:SetBinaryState>
  </s:Body>
</s:Envelope>

Interessant ist eigentlich nur der rot markierte Eintrag (BinaryState). Wird hier eine "0" übermittelt, heißt das ausschalten. Eine "1" bedeutet einschalten.

Implementierung

Das Objekt UrsWemoManager ist das Objekt, mit dem die Applikation kommuniziert. Die dort registrierten Objekte der UrsWemoSwitch-Klasse werden von UrsWemoManager angesteuert bzw. kommunizieren autonom mit Alexa.

UrsWemoManager

addDevice() fügt ganz unspannend ein neues Device zur internen Geräteliste hinzu und übergibt die eingestellten Print-Referenzen (ohne Protokoll-Ausgaben):

void UrsWemoManagerClass::addDevice(UrsWemoSwitch & switchDevice) {
  if (numOfSwitchs >= MAX_SWITCHES) { // nicht mehr als vorgesehen!
    errDevice->println(F("*** Can't add an additional switch! ***"));
    return;
  } // if numOfSwitchs

  switchDevice.OutDevice = outDevice;
  switchDevice.ErrDevice = errDevice;
 
  switches[numOfSwitchs] = &switchDevice;
  numOfSwitchs++;
}

begin() meldet sich an der Multicast-Gruppe auf der Alexa sendet (239.255.255.250:1900) mit der eigenen IP an (s. IGMP) und startet die vorher hinzugefügten Switches.

bool UrsWemoManagerClass::begin() {
  if (UDP.beginMulticast(WiFi.localIP(), ipMulti, portMulti)) {
    ...

    for (int n = 0; n < numOfSwitchs; n++) { // Switches initialisieren
      switches[n]->begin();
    } // for n
    return true;
  } // if UPD

  return false;
}

handle() lauscht, ob neue UPD-Pakete (UDP.parsePacket()) mit den Inhalten "M-SEARCH" und "urn:Belkin:device:**" eingetroffen sind und fordert dann die registrierten Switches auf, sich bei Alexa zu melden (switches[n]->respondToSearch(...);). Zum Schluss wird für jeden Switch die Methode (Switch::) handle() aufgerufen, damit diese ggf. eingetroffene Anfragen an den dort hinterlegten Webserver beantworten können.

void UrsWemoManagerClass::handle() { // regelmäßig nach eingetroffenen Paketen schauen
  int packetSize = UDP.parsePacket();
  if (packetSize > 0) { // UDP-Paket erhalten
    IPAddress senderIP = UDP.remoteIP();
    unsigned int senderPort = UDP.remotePort();

    // Das UDP-Paket einlesen
    char packetBuffer[512];
    UDP.read(packetBuffer, packetSize);

    // Überprüfen, ob dies eine 'M-SEARCH'-Abfrage für WeMo-Geräte ist
    String request = String((char *)packetBuffer);
    if (request.indexOf("M-SEARCH") > -1) {
      if (request.indexOf("urn:Belkin:device:**") > -1) {
        outDevice->println("Got UDP Belkin Request..");
        for (int n = 0; n < numOfSwitchs; n++) {
          switches[n]->respondToSearch(senderIP, senderPort);
        } // for
      } // if urn:Belkin:device
    } // if M-SEARCH
  } // if paketSize

  // Den Switches Rechenzeit gewähren
  for (int n = 0; n < numOfSwitchs; n++) {
    switches[n]->handle();
  } // for
}

UrsWemoSwitch

Mit der Instanziierung des Switch-Objektes und dessen Registrierung beim UrsWemoManager ist alles erledigt, was die Applikation tun muss. UrsWemoManager ist als friend class festgelegt, damit der Manager zugriff auf die privaten Elemente erhält.

Der Konstruktor UrsWemoSwitch(String alexaInvokeName, unsigned int webServerPort) übernimmt die Einrichtung eines Webserver auf dem angegeben Port. Für die Pfade "/", "/setup" (Setup-Daten) und "/upnp/control/basicevent1" (Schalt-Anweisung) werden Handler-Routinen hinterlegt. Eine Anfrage auf root ("/") habe ich zwar bisher noch nie erhalten, habe mich aber auch nicht getraut sie wegzulassen.

UrsWemoSwitch::UrsWemoSwitch(String alexaInvokeName, unsigned int webServerPort) {
  char uuid[64];
  sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%06x"), ESP.getChipId() & 0xFFFFFF);
  serialNo = String(uuid);

  UUID = "Socket-1_0-" + serialNo + "-" + String(webServerPort);

  invokeName = alexaInvokeName;
  this->webServerPort = webServerPort;

  // Webserver initialisieren
  server = new ESP8266WebServer(webServerPort);

  server->on("/", [&]() { handleRoot(); });
  server->on("/setup.xml", [&]() { handleSetupXml(); });
  server->on("/upnp/control/basicevent1", [&]() {handleUpnpControl(); });
}

Der für die Applikation relevante Konstruktor besitzt zwei weitere Parameter: uint8_t digitalPin und uint8_t highValue = HIGH. Dieser Konstruktor nutzt den vorher angeführten zur Initialisierung der Netzwerk-Elemente und setzt den angegebenen Pin in den Output-Modus. Außerdem wird festgelegt, welcher Wert (HIGH oder LOW) auf dem Pin ausgegeben werden muss, wenn die Anweisung "Einschalten" eintrifft.

UrsWemoSwitch::UrsWemoSwitch(String alexaInvokeName, unsigned int webServerPort, uint8_t dPin, uint8_t hValue) 
                            : UrsWemoSwitch(alexaInvokeName, webServerPort) {
  digitalPin = dPin;
  pinMode(dPin, OUTPUT);

  if (hValue) {
    onValue = HIGH;
    offValue = LOW;
  }
  else {
    onValue = LOW;
    offValue = HIGH;
  }
}

begin() startet ausschließlich den Webserver (ohne Protokoll-Ausgaben):

void UrsWemoSwitch::begin()
{ server->begin();
}

handle() teilt dem Webserver Rechenzeit zu:

void UrsWemoSwitch::handle() {
  if (server != NULL) {
    server->handleClient();
  }
}

onSwitchOn() und onSwitchOff() schalten den Pin  (ohne Protokoll-Ausgaben):

void UrsWemoSwitch::onSwitchOn() {
  digitalWrite(digitalPin, onValue);
}

void UrsWemoSwitch::onSwitchOff() {
  digitalWrite(digitalPin, offValue);
}

Beide onSwitchxx Methoden sind öffentlich, damit sie ggf. auch von der Applikation bedient werden können.

Die Handler-Methoden bauen die im Abschnitt Funktionsweise beschriebenen HTTP-Antworten auf und versenden sie.

Weitere nützliche Links zur Alexa-Steuerung

Ben's Place: Wemo Archive

Amazon Echo and Home Automation

Virtual WeMo Code for Amazon Echo