From 255cef8685b03f08533a0953300784f162dd2d33 Mon Sep 17 00:00:00 2001 From: cschwinne Date: Tue, 21 Feb 2017 23:59:47 +0100 Subject: [PATCH] Added Alexa support --- TODO.txt | 3 +- readme.md | 11 +- wled00/CallbackFunction.h | 8 ++ wled00/Switch.cpp | 201 ++++++++++++++++++++++++++++++ wled00/Switch.h | 36 ++++++ wled00/UpnpBroadcastResponder.cpp | 86 +++++++++++++ wled00/UpnpBroadcastResponder.h | 20 +++ wled00/wled00.ino | 17 ++- wled00/wled03_set.ino | 12 +- wled00/wled05_init.ino | 2 + wled00/wled08_led.ino | 2 +- wled00/wled12_alexa.ino | 44 +++++++ 12 files changed, 435 insertions(+), 7 deletions(-) create mode 100644 wled00/CallbackFunction.h create mode 100644 wled00/Switch.cpp create mode 100644 wled00/Switch.h create mode 100644 wled00/UpnpBroadcastResponder.cpp create mode 100644 wled00/UpnpBroadcastResponder.h create mode 100644 wled00/wled12_alexa.ino diff --git a/TODO.txt b/TODO.txt index 5b0da6b8..8affbdec 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,7 +1,6 @@ captive portal for ap -alexa support +alexa settings ntp bug -bri bug simple slide transition additional color picker field implement HSB slider option diff --git a/readme.md b/readme.md index 2eea6cba..57cab5c4 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,8 @@ Additions for V0.3 (nearly complete!) - Support for power pushbutton - Full OTA software update capability - Password protected OTA page for added security (OTA lock) -- NTP and experimental analog clock function +- Alexa smart home device server +- (not working) NTP and experimental analog clock function Compile settings: Board: WeMos D1 mini @@ -66,6 +67,14 @@ Add one or multiple of the following parameters after the base url to change val ("&I=<0-255>&I2=<0-255>" experimental individual LED range control) +Licensed under the MIT license +Uses libraries: +ESP8266 Arduino Core +WS2812FX by kitesurfer1404 (Aircoookie fork) +Timezone library by JChristensen +arduino-esp8266-alexa-multiple-wemo-switch by kakopappa + + Software update procedure: Method 1: Reflash the new update source via USB. diff --git a/wled00/CallbackFunction.h b/wled00/CallbackFunction.h new file mode 100644 index 00000000..df9feb1f --- /dev/null +++ b/wled00/CallbackFunction.h @@ -0,0 +1,8 @@ +#ifndef CALLBACKFUNCTION_H +#define CALLBACKFUNCTION_H + +#include + +typedef void (*CallbackFunction) (); + +#endif diff --git a/wled00/Switch.cpp b/wled00/Switch.cpp new file mode 100644 index 00000000..00b38215 --- /dev/null +++ b/wled00/Switch.cpp @@ -0,0 +1,201 @@ +#include "Switch.h" +#include "CallbackFunction.h" + + + +//<> +Switch::Switch(){ + Serial.println("default constructor called"); +} +//Switch::Switch(String alexaInvokeName,unsigned int port){ +Switch::Switch(String alexaInvokeName, unsigned int port, CallbackFunction oncb, CallbackFunction offcb){ + uint32_t chipId = ESP.getChipId(); + char uuid[64]; + sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%02x%02x%02x"), + (uint16_t) ((chipId >> 16) & 0xff), + (uint16_t) ((chipId >> 8) & 0xff), + (uint16_t) chipId & 0xff); + + serial = String(uuid); + persistent_uuid = "Socket-1_0-" + serial+"-"+ String(port); + + device_name = alexaInvokeName; + localPort = port; + onCallback = oncb; + offCallback = offcb; + + startWebServer(); +} + + + +//<> +Switch::~Switch(){/*nothing to destruct*/} + +void Switch::serverLoop(){ + if (server != NULL) { + server->handleClient(); + delay(1); + } +} + +void Switch::startWebServer(){ + server = new ESP8266WebServer(localPort); + + server->on("/", [&]() { + handleRoot(); + }); + + + server->on("/setup.xml", [&]() { + handleSetupXml(); + }); + + server->on("/upnp/control/basicevent1", [&]() { + handleUpnpControl(); + }); + + server->on("/eventservice.xml", [&]() { + handleEventservice(); + }); + + //server->onNotFound(handleNotFound); + server->begin(); + + Serial.println("WebServer started on port: "); + Serial.println(localPort); +} + +void Switch::handleEventservice(){ + Serial.println(" ########## Responding to eventservice.xml ... ########\n"); + + String eventservice_xml = "" + "" + "" + "SetBinaryState" + "" + "" + "" + "BinaryState" + "BinaryState" + "in" + "" + "" + "" + "" + "BinaryState" + "Boolean" + "0" + "" + "" + "level" + "string" + "0" + "" + "" + "" + "\r\n" + "\r\n"; + + server->send(200, "text/plain", eventservice_xml.c_str()); +} + +void Switch::handleUpnpControl(){ + Serial.println("########## Responding to /upnp/control/basicevent1 ... ##########"); + + //for (int x=0; x <= HTTP.args(); x++) { + // Serial.println(HTTP.arg(x)); + //} + + String request = server->arg(0); + Serial.print("request:"); + Serial.println(request); + + if(request.indexOf("1") > 0) { + Serial.println("Got Turn on request"); + onCallback(); + } + + if(request.indexOf("0") > 0) { + Serial.println("Got Turn off request"); + offCallback(); + } + + server->send(200, "text/plain", ""); +} + +void Switch::handleRoot(){ + server->send(200, "text/plain", "You should tell Alexa to discover devices"); +} + +void Switch::handleSetupXml(){ + Serial.println(" ########## Responding to setup.xml ... ########\n"); + + IPAddress localIP = WiFi.localIP(); + char s[16]; + sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); + + String setup_xml = "" + "" + "" + "urn:Belkin:device:controllee:1" + ""+ device_name +"" + "Belkin International Inc." + "Emulated Socket" + "3.1415" + "uuid:"+ persistent_uuid +"" + "221517K0101769" + "0" + "" + "" + "urn:Belkin:service:basicevent:1" + "urn:Belkin:serviceId:basicevent1" + "/upnp/control/basicevent1" + "/upnp/event/basicevent1" + "/eventservice.xml" + "" + "" + "" + "\r\n" + "\r\n"; + + server->send(200, "text/xml", setup_xml.c_str()); + + Serial.print("Sending :"); + Serial.println(setup_xml); +} + +String Switch::getAlexaInvokeName() { + return device_name; +} + +void Switch::respondToSearch(IPAddress& senderIP, unsigned int senderPort) { + Serial.println(""); + Serial.print("Sending response to "); + Serial.println(senderIP); + Serial.print("Port : "); + Serial.println(senderPort); + + IPAddress localIP = WiFi.localIP(); + char s[16]; + sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); + + String response = + "HTTP/1.1 200 OK\r\n" + "CACHE-CONTROL: max-age=86400\r\n" + "DATE: Sat, 26 Nov 2016 04:56:29 GMT\r\n" + "EXT:\r\n" + "LOCATION: http://" + String(s) + ":" + String(localPort) + "/setup.xml\r\n" + "OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n" + "01-NLS: b9200ebb-736d-4b93-bf03-835149d13983\r\n" + "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n" + "ST: urn:Belkin:device:**\r\n" + "USN: uuid:" + persistent_uuid + "::urn:Belkin:device:**\r\n" + "X-User-Agent: redsonic\r\n\r\n"; + + UDP.beginPacket(senderIP, senderPort); + UDP.write(response.c_str()); + UDP.endPacket(); + + Serial.println("Response sent !"); +} diff --git a/wled00/Switch.h b/wled00/Switch.h new file mode 100644 index 00000000..ebe2039b --- /dev/null +++ b/wled00/Switch.h @@ -0,0 +1,36 @@ +#ifndef SWITCH_H +#define SWITCH_H + +#include +#include +#include +#include +#include "CallbackFunction.h" + + +class Switch { +private: + ESP8266WebServer *server = NULL; + WiFiUDP UDP; + String serial; + String persistent_uuid; + String device_name; + unsigned int localPort; + CallbackFunction onCallback; + CallbackFunction offCallback; + + void startWebServer(); + void handleEventservice(); + void handleUpnpControl(); + void handleRoot(); + void handleSetupXml(); +public: + Switch(); + Switch(String alexaInvokeName, unsigned int port, CallbackFunction onCallback, CallbackFunction offCallback); + ~Switch(); + String getAlexaInvokeName(); + void serverLoop(); + void respondToSearch(IPAddress& senderIP, unsigned int senderPort); +}; + +#endif diff --git a/wled00/UpnpBroadcastResponder.cpp b/wled00/UpnpBroadcastResponder.cpp new file mode 100644 index 00000000..2a721e33 --- /dev/null +++ b/wled00/UpnpBroadcastResponder.cpp @@ -0,0 +1,86 @@ +#include "UpnpBroadcastResponder.h" +#include "Switch.h" +#include + +// Multicast declarations +IPAddress ipMulti(239, 255, 255, 250); +const unsigned int portMulti = 1900; +char packetBuffer[512]; + +#define MAX_SWITCHES 14 +Switch switches[MAX_SWITCHES] = {}; +int numOfSwitchs = 0; + +//#define numOfSwitchs (sizeof(switches)/sizeof(Switch)) //array size + +//<> +UpnpBroadcastResponder::UpnpBroadcastResponder(){ + +} + +//<> +UpnpBroadcastResponder::~UpnpBroadcastResponder(){/*nothing to destruct*/} + +bool UpnpBroadcastResponder::beginUdpMulticast(){ + boolean state = false; + + Serial.println("Begin multicast .."); + + if(UDP.beginMulticast(WiFi.localIP(), ipMulti, portMulti)) { + Serial.print("Udp multicast server started at "); + Serial.print(ipMulti); + Serial.print(":"); + Serial.println(portMulti); + + state = true; + } + else{ + Serial.println("Connection failed"); + } + + return state; +} + +//Switch *ptrArray; + +void UpnpBroadcastResponder::addDevice(Switch& device) { + Serial.print("Adding switch : "); + Serial.print(device.getAlexaInvokeName()); + Serial.print(" index : "); + Serial.println(numOfSwitchs); + + switches[numOfSwitchs] = device; + numOfSwitchs++; +} + +void UpnpBroadcastResponder::serverLoop(){ + int packetSize = UDP.parsePacket(); + if (packetSize <= 0) + return; + + IPAddress senderIP = UDP.remoteIP(); + unsigned int senderPort = UDP.remotePort(); + + // read the packet into the buffer + UDP.read(packetBuffer, packetSize); + + // check if this is a M-SEARCH for WeMo device + String request = String((char *)packetBuffer); + + if(request.indexOf('M-SEARCH') > 0) { + if(request.indexOf("urn:Belkin:device:**") > 0) { + Serial.println("Got UDP Belkin Request.."); + + // int arrSize = sizeof(switchs) / sizeof(Switch); + + for(int n = 0; n < numOfSwitchs; n++) { + Switch &sw = switches[n]; + + if (&sw != NULL) { + sw.respondToSearch(senderIP, senderPort); + } + } + } + } +} + diff --git a/wled00/UpnpBroadcastResponder.h b/wled00/UpnpBroadcastResponder.h new file mode 100644 index 00000000..b8c80397 --- /dev/null +++ b/wled00/UpnpBroadcastResponder.h @@ -0,0 +1,20 @@ +#ifndef UPNPBROADCASTRESPONDER_H +#define UPNPBROADCASTRESPONDER_H + +#include +#include +#include +#include "Switch.h" + +class UpnpBroadcastResponder { +private: + WiFiUDP UDP; +public: + UpnpBroadcastResponder(); + ~UpnpBroadcastResponder(); + bool beginUdpMulticast(); + void serverLoop(); + void addDevice(Switch& device); +}; + +#endif diff --git a/wled00/wled00.ino b/wled00/wled00.ino index 28303aed..116d889a 100644 --- a/wled00/wled00.ino +++ b/wled00/wled00.ino @@ -17,6 +17,9 @@ #include #include "htmls00.h" #include "htmls01.h" +#include "switch.h" +#include "UpnpBroadcastResponder.h" +#include "CallbackFunction.h" //to toggle usb serial debug (un)comment following line #define DEBUG @@ -37,7 +40,7 @@ * @author Christian Schwinne */ //Hardware-settings (only changeble via code) -#define LEDCOUNT 11 +#define LEDCOUNT 9 #define MAXDIRECT 52 //for direct access like arls, should be >= LEDCOUNT uint8_t buttonPin = 0; //needs pull-up uint8_t auxPin = 15; //use e.g. for external relay @@ -99,9 +102,14 @@ boolean overlayReverse = true; uint8_t overlaySpeed = 200; boolean useGammaCorrectionBri = true; boolean useGammaCorrectionRGB = true; -int arlsOffset = -21; //10: -22 assuming arls52 +int arlsOffset = -22; //10: -22 assuming arls52 boolean realtimeEnabled = true; +//alexa +boolean alexaEnabled = true; +String alexaInvocationName = "Schloss"; +boolean alexaNotify = false; + double transitionResolution = 0.011; //Internal vars @@ -153,6 +161,10 @@ uint8_t auxTime = 0; unsigned long auxStartTime; boolean auxActive, auxActiveBefore; +//alexa +Switch *alexa = NULL; +UpnpBroadcastResponder upnpBroadcastResponder; + ESP8266WebServer server(80); ESP8266HTTPUpdateServer httpUpdater; WiFiUDP notifierUdp; @@ -220,6 +232,7 @@ void loop() { handleButton(); handleNetworkTime(); handleOverlays(); + handleAlexa(); strip.service(); //DEBUG diff --git a/wled00/wled03_set.ino b/wled00/wled03_set.ino index e606be27..5d4ed52a 100644 --- a/wled00/wled03_set.ino +++ b/wled00/wled03_set.ino @@ -305,7 +305,17 @@ boolean handleSet(String req) } } } - XML_response(); + pos = req.indexOf("IN"); + if (pos < 1) + { + XML_response(); + } + pos = req.indexOf("NN"); + if (pos > 0) + { + colorUpdated(5); + return true; + } if (effectUpdated) { colorUpdated(6); diff --git a/wled00/wled05_init.ino b/wled00/wled05_init.ino index b13d52fb..f9999b15 100644 --- a/wled00/wled05_init.ino +++ b/wled00/wled05_init.ino @@ -131,6 +131,8 @@ void wledInit() DEBUG_PRINTLN("HTTP server started"); // Add service to MDNS MDNS.addService("http", "tcp", 80); + //Init alexa service + alexaInit(); // Initialize NeoPixel Strip strip.init(); strip.setMode(effectCurrent); diff --git a/wled00/wled08_led.ino b/wled00/wled08_led.ino index bec79781..41636fb1 100644 --- a/wled00/wled08_led.ino +++ b/wled00/wled08_led.ino @@ -35,7 +35,7 @@ void setLedsStandard() void colorUpdated(int callMode) { - //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (no not.) 6: fx changed + //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (no not.) (NN)6: fx changed if (col[0] == col_it[0] && col[1] == col_it[1] && col[2] == col_it[2] && bri == bri_it) { if (callMode == 6) notify(6); diff --git a/wled00/wled12_alexa.ino b/wled00/wled12_alexa.ino new file mode 100644 index 00000000..7bd87072 --- /dev/null +++ b/wled00/wled12_alexa.ino @@ -0,0 +1,44 @@ +void alexaOn(); +void alexaOff(); + +void alexaInit() +{ + if (alexaEnabled && WiFi.status() == WL_CONNECTED) + { + upnpBroadcastResponder.beginUdpMulticast(); + alexa = new Switch(alexaInvocationName, 81, alexaOn, alexaOff); + upnpBroadcastResponder.addDevice(*alexa); + } +} + +void handleAlexa() +{ + if (alexaEnabled && WiFi.status() == WL_CONNECTED) + { + upnpBroadcastResponder.serverLoop(); + alexa->serverLoop(); + } +} + +void alexaOn() +{ + if (alexaNotify) + { + handleSet("win&T=1&IN"); + } else + { + handleSet("win&T=1&NN&IN"); + } +} + +void alexaOff() +{ + if (alexaNotify) + { + handleSet("win&T=0&IN"); + } else + { + handleSet("win&T=0&NN&IN"); + } +} +