diff --git a/platformio.ini b/platformio.ini index 015894a7..3cad5ee1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,7 +27,7 @@ lib_deps_external = #E131@1.0.0 #webserver FastLED@3.2.1 - NeoPixelBus@2.3.4 + NeoPixelBus@2.4.1 #PubSubClient@2.7 #Time@1.5 #Timezone@1.2.1 diff --git a/readme.md b/readme.md index 84381af0..4fe717ac 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ ![WLED logo](https://raw.githubusercontent.com/Aircoookie/WLED/master/wled_logo.png) -## Welcome to my project WLED! (v0.8.2) +## Welcome to my project WLED! (v0.8.3-dev) A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control NeoPixel (WS2812B) LEDs! @@ -25,7 +25,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control - E1.31 - Hyperion - UDP realtime -- Alexa smart device (including dimming) +- Alexa voice control (including dimming and color) - Sync to Philips hue lights - Adalight (PC ambilight via serial) - Sync color of multiple WLED devices (UDP notifier) diff --git a/wled00/NpbWrapper.h b/wled00/NpbWrapper.h index c33c869c..92ce1007 100644 --- a/wled00/NpbWrapper.h +++ b/wled00/NpbWrapper.h @@ -24,7 +24,7 @@ #else //esp8266 //autoselect the right method depending on strip pin #if LEDPIN == 2 - #define PIXELMETHOD NeoEsp8266Uart800KbpsMethod + #define PIXELMETHOD NeoEsp8266Uart1Ws2813Method //if you get an error here, please update to Neopixelbus v2.4.0+ #elif LEDPIN == 3 #define PIXELMETHOD NeoEsp8266Dma800KbpsMethod #else diff --git a/wled00/WS2812FX.cpp b/wled00/WS2812FX.cpp index 1ec2d251..9857876b 100644 --- a/wled00/WS2812FX.cpp +++ b/wled00/WS2812FX.cpp @@ -42,6 +42,9 @@ #include "WS2812FX.h" #include "palettes.h" + +#define LED_SKIP_AMOUNT 1 + void WS2812FX::init(bool supportWhite, uint16_t countPixels, bool skipFirst) { if (supportWhite == _rgbwMode && countPixels == _length && _locked != NULL) return; @@ -49,14 +52,19 @@ void WS2812FX::init(bool supportWhite, uint16_t countPixels, bool skipFirst) _rgbwMode = supportWhite; _skipFirstMode = skipFirst; _length = countPixels; - if (_skipFirstMode) _length++; + uint8_t ty = 1; if (supportWhite) ty =2; - bus->Begin((NeoPixelType)ty, _length); + uint16_t lengthRaw = _length; + if (_skipFirstMode) lengthRaw += LED_SKIP_AMOUNT; + bus->Begin((NeoPixelType)ty, lengthRaw); + if (_locked != NULL) delete _locked; _locked = new byte[_length]; + _segments[0].start = 0; _segments[0].stop = _length -1; + unlockAll(); setBrightness(_brightness); _running = true; @@ -119,7 +127,12 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) } if (!_cronixieMode) { - if (_skipFirstMode) {i++;if(i==1)bus->SetPixelColor(i, RgbwColor(0,0,0,0));} + if (_skipFirstMode) + { + if (i < LED_SKIP_AMOUNT) bus->SetPixelColor(i, RgbwColor(0,0,0,0)); + i += LED_SKIP_AMOUNT; + } + bus->SetPixelColor(i, RgbwColor(r,g,b,w)); } else { if(i>6)return; @@ -132,27 +145,28 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) byte w2 = (_segments[0].colors[1] >>24) & 0xFF; for (int j=o; j< o+19; j++) { - bus->SetPixelColor((_skipFirstMode)?j+1:j,RgbwColor(r2,g2,b2,w2)); + bus->SetPixelColor(j, RgbwColor(r2,g2,b2,w2)); } } else { for (int j=o; j< o+19; j++) { - bus->SetPixelColor((_skipFirstMode)?j+1:j,RgbwColor(0,0,0,0)); + bus->SetPixelColor(j, RgbwColor(0,0,0,0)); } } + if (_skipFirstMode) o += LED_SKIP_AMOUNT; switch(_cronixieDigits[i]) { - case 0: bus->SetPixelColor((_skipFirstMode)?o+6:o+5,RgbwColor(r,g,b,w)); break; - case 1: bus->SetPixelColor((_skipFirstMode)?o+1:o+0,RgbwColor(r,g,b,w)); break; - case 2: bus->SetPixelColor((_skipFirstMode)?o+7:o+6,RgbwColor(r,g,b,w)); break; - case 3: bus->SetPixelColor((_skipFirstMode)?o+2:o+1,RgbwColor(r,g,b,w)); break; - case 4: bus->SetPixelColor((_skipFirstMode)?o+8:o+7,RgbwColor(r,g,b,w)); break; - case 5: bus->SetPixelColor((_skipFirstMode)?o+3:o+2,RgbwColor(r,g,b,w)); break; - case 6: bus->SetPixelColor((_skipFirstMode)?o+9:o+8,RgbwColor(r,g,b,w)); break; - case 7: bus->SetPixelColor((_skipFirstMode)?o+4:o+3,RgbwColor(r,g,b,w)); break; - case 8: bus->SetPixelColor((_skipFirstMode)?o+10:o+9,RgbwColor(r,g,b,w)); break; - case 9: bus->SetPixelColor((_skipFirstMode)?o+5:o+4,RgbwColor(r,g,b,w)); break; + case 0: bus->SetPixelColor(o+5, RgbwColor(r,g,b,w)); break; + case 1: bus->SetPixelColor(o+0, RgbwColor(r,g,b,w)); break; + case 2: bus->SetPixelColor(o+6, RgbwColor(r,g,b,w)); break; + case 3: bus->SetPixelColor(o+1, RgbwColor(r,g,b,w)); break; + case 4: bus->SetPixelColor(o+7, RgbwColor(r,g,b,w)); break; + case 5: bus->SetPixelColor(o+2, RgbwColor(r,g,b,w)); break; + case 6: bus->SetPixelColor(o+8, RgbwColor(r,g,b,w)); break; + case 7: bus->SetPixelColor(o+3, RgbwColor(r,g,b,w)); break; + case 8: bus->SetPixelColor(o+9, RgbwColor(r,g,b,w)); break; + case 9: bus->SetPixelColor(o+4, RgbwColor(r,g,b,w)); break; } } } @@ -340,7 +354,7 @@ uint32_t WS2812FX::getColor(void) { uint32_t WS2812FX::getPixelColor(uint16_t i) { if (_reverseMode) i = _length- 1 -i; - if (_skipFirstMode) i++; + if (_skipFirstMode) i += LED_SKIP_AMOUNT; if (_cronixieMode) { if(i>6)return 0; diff --git a/wled00/html_settings.h b/wled00/html_settings.h index a83552d4..e79a29f4 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -437,7 +437,7 @@ HTTP traffic is unencrypted. An attacker in the same network can intercept form
Enable ArduinoOTA:

About

-WLED version 0.8.2

+WLED version 0.8.3

Contributors, dependencies and special thanks
A huge thank you to everyone who helped me create WLED!

(c) 2016-2018 Christian Schwinne
diff --git a/wled00/src/dependencies/espalexa/Espalexa.h b/wled00/src/dependencies/espalexa/Espalexa.h new file mode 100644 index 00000000..3b817415 --- /dev/null +++ b/wled00/src/dependencies/espalexa/Espalexa.h @@ -0,0 +1,486 @@ +#ifndef Espalexa_h +#define Espalexa_h + +/* + * Alexa Voice On/Off/Brightness/Color Control. Emulates a Philips Hue bridge to Alexa. + * + * This was put together from these two excellent projects: + * https://github.com/kakopappa/arduino-esp8266-alexa-wemo-switch + * https://github.com/probonopd/ESP8266HueEmulator + */ +/* + * @title Espalexa library + * @version 2.3.3 + * @author Christian Schwinne + * @license MIT + * @contributors d-999 + */ + +#include "Arduino.h" + +//you can use these defines for library config in your sketch. Just use them before #include +//#define ESPALEXA_ASYNC + +#ifndef ESPALEXA_MAXDEVICES + #define ESPALEXA_MAXDEVICES 10 //this limit only has memory reasons, set it higher should you need to +#endif + +//#define ESPALEXA_DEBUG + +#ifdef ESPALEXA_ASYNC + #ifdef ARDUINO_ARCH_ESP32 + #include + #else + #include + #endif + #include +#else + #ifdef ARDUINO_ARCH_ESP32 + #include + #include //if you get an error here please update to ESP32 arduino core 1.0.0 + #else + #include + #include + #endif +#endif +#include + +#ifdef ESPALEXA_DEBUG + #pragma message "Espalexa 2.3.3 debug mode" + #define EA_DEBUG(x) Serial.print (x) + #define EA_DEBUGLN(x) Serial.println (x) +#else + #define EA_DEBUG(x) + #define EA_DEBUGLN(x) +#endif + +#include "EspalexaDevice.h" + +class Espalexa { +private: + //private member vars + #ifdef ESPALEXA_ASYNC + AsyncWebServer* serverAsync; + AsyncWebServerRequest* server; //this saves many #defines + String body = ""; + #elif defined ARDUINO_ARCH_ESP32 + WebServer* server; + #else + ESP8266WebServer* server; + #endif + uint8_t currentDeviceCount = 0; + + EspalexaDevice* devices[ESPALEXA_MAXDEVICES] = {}; + //Keep in mind that Device IDs go from 1 to DEVICES, cpp arrays from 0 to DEVICES-1!! + + WiFiUDP espalexaUdp; + IPAddress ipMulti; + bool udpConnected = false; + char packetBuffer[255]; //buffer to hold incoming udp packet + String escapedMac=""; //lowercase mac address + + //private member functions + String deviceJsonString(uint8_t deviceId) + { + if (deviceId < 1 || deviceId > currentDeviceCount) return "{}"; //error + EspalexaDevice* dev = devices[deviceId-1]; + String json = "{\"type\":\""; + json += dev->isColorDevice() ? "Extended color light" : "Dimmable light"; + json += "\",\"manufacturername\":\"OpenSource\",\"swversion\":\"0.1\",\"name\":\""; + json += dev->getName(); + json += "\",\"uniqueid\":\""+ WiFi.macAddress() +"-"+ (deviceId+1) ; + json += "\",\"modelid\":\"LST001\",\"state\":{\"on\":"; + json += boolString(dev->getValue()) +",\"bri\":"+ (String)(dev->getLastValue()-1) ; + if (dev->isColorDevice()) + { + json += ",\"xy\":[0.00000,0.00000],\"colormode\":\""; + json += (dev->isColorTemperatureMode()) ? "ct":"hs"; + json += "\",\"effect\":\"none\",\"ct\":" + (String)(dev->getCt()) + ",\"hue\":" + (String)(dev->getHue()) + ",\"sat\":" + (String)(dev->getSat()); + } + json +=",\"alert\":\"none\",\"reachable\":true}}"; + return json; + } + + //Espalexa status page /espalexa + void servePage() + { + EA_DEBUGLN("HTTP Req espalexa ...\n"); + String res = "Hello from Espalexa!\r\n\r\n"; + for (int i=0; igetName() + "): " + String(devices[i]->getValue()) + "\r\n"; + } + res += "\r\nFree Heap: " + (String)ESP.getFreeHeap(); + res += "\r\nUptime: " + (String)millis(); + res += "\r\n\r\nEspalexa library v2.3.3 by Christian Schwinne 2019"; + server->send(200, "text/plain", res); + } + + //not found URI (only if internal webserver is used) + void serveNotFound() + { + EA_DEBUGLN("Not-Found HTTP call:"); + #ifndef ESPALEXA_ASYNC + EA_DEBUGLN("URI: " + server->uri()); + EA_DEBUGLN("Body: " + server->arg(0)); + if(!handleAlexaApiCall(server->uri(), server->arg(0))) + #else + EA_DEBUGLN("URI: " + server->url()); + EA_DEBUGLN("Body: " + body); + if(!handleAlexaApiCall(server)) + #endif + server->send(404, "text/plain", "Not Found (espalexa-internal)"); + } + + //send description.xml device property page + void serveDescription() + { + EA_DEBUGLN("# Responding to description.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 = "" + "" + "10" + "http://"+ String(s) +":80/" + "" + "urn:schemas-upnp-org:device:Basic:1" + "Philips hue ("+ String(s) +")" + "Royal Philips Electronics" + "http://www.philips.com" + "Philips hue Personal Wireless Lighting" + "Philips hue bridge 2012" + "929000226503" + "http://www.meethue.com" + ""+ escapedMac +"" + "uuid:2f402f80-da50-11e1-9b23-"+ escapedMac +"" + "index.html" + "" + " " + " image/png" + " 48" + " 48" + " 24" + " hue_logo_0.png" + " " + " " + " image/png" + " 120" + " 120" + " 24" + " hue_logo_3.png" + " " + "" + "" + ""; + + server->send(200, "text/xml", setup_xml.c_str()); + + EA_DEBUG("Sending :"); + EA_DEBUGLN(setup_xml); + } + + //init the server + void startHttpServer() + { + #ifdef ESPALEXA_ASYNC + if (serverAsync == nullptr) { + serverAsync = new AsyncWebServer(80); + serverAsync->onNotFound([=](AsyncWebServerRequest *request){server = request; serveNotFound();}); + } + + serverAsync->onRequestBody([=](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ + char b[len +1]; + b[len] = 0; + memcpy(b, data, len); + body = b; //save the body so we can use it for the API call + EA_DEBUG("Received body: "); + EA_DEBUGLN(body); + }); + serverAsync->on("/espalexa", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; servePage();}); + serverAsync->on("/description.xml", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; serveDescription();}); + serverAsync->begin(); + + #else + if (server == nullptr) { + #ifdef ARDUINO_ARCH_ESP32 + server = new WebServer(80); + #else + server = new ESP8266WebServer(80); + #endif + server->onNotFound([=](){serveNotFound();}); + } + + server->on("/espalexa", HTTP_GET, [=](){servePage();}); + server->on("/description.xml", HTTP_GET, [=](){serveDescription();}); + server->begin(); + #endif + } + + //called when Alexa sends ON command + void alexaOn(uint8_t deviceId) + { + devices[deviceId-1]->setValue(devices[deviceId-1]->getLastValue()); + devices[deviceId-1]->setPropertyChanged(1); + devices[deviceId-1]->doCallback(); + } + + //called when Alexa sends OFF command + void alexaOff(uint8_t deviceId) + { + devices[deviceId-1]->setValue(0); + devices[deviceId-1]->setPropertyChanged(2); + devices[deviceId-1]->doCallback(); + } + + //called when Alexa sends BRI command + void alexaDim(uint8_t deviceId, uint8_t briL) + { + if (briL == 255) + { + devices[deviceId-1]->setValue(255); + } else { + devices[deviceId-1]->setValue(briL+1); + } + devices[deviceId-1]->setPropertyChanged(3); + devices[deviceId-1]->doCallback(); + } + + //called when Alexa sends HUE command + void alexaCol(uint8_t deviceId, uint16_t hue, uint8_t sat) + { + devices[deviceId-1]->setColor(hue, sat); + devices[deviceId-1]->setPropertyChanged(4); + devices[deviceId-1]->doCallback(); + } + + //called when Alexa sends CT command (color temperature) + void alexaCt(uint8_t deviceId, uint16_t ct) + { + devices[deviceId-1]->setColor(ct); + devices[deviceId-1]->setPropertyChanged(5); + devices[deviceId-1]->doCallback(); + } + + //respond to UDP SSDP M-SEARCH + void respondToSearch() + { + 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" + "EXT:\r\n" + "CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL + "LOCATION: http://"+ String(s) +":80/description.xml\r\n" + "SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber + "hue-bridgeid: "+ escapedMac +"\r\n" + "ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType + "USN: uuid:2f402f80-da50-11e1-9b23-"+ escapedMac +"::upnp:rootdevice\r\n" // _uuid::_deviceType + "\r\n"; + + espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort()); + #ifdef ARDUINO_ARCH_ESP32 + espalexaUdp.write((uint8_t*)response.c_str(), response.length()); + #else + espalexaUdp.write(response.c_str()); + #endif + espalexaUdp.endPacket(); + } + + String boolString(bool st) + { + return(st)?"true":"false"; + } + +public: + Espalexa(){} + + //initialize interfaces + #ifdef ESPALEXA_ASYNC + bool begin(AsyncWebServer* externalServer = nullptr) + #elif defined ARDUINO_ARCH_ESP32 + bool begin(WebServer* externalServer = nullptr) + #else + bool begin(ESP8266WebServer* externalServer = nullptr) + #endif + { + EA_DEBUGLN("Espalexa Begin..."); + EA_DEBUG("MAXDEVICES "); + EA_DEBUGLN(ESPALEXA_MAXDEVICES); + escapedMac = WiFi.macAddress(); + escapedMac.replace(":", ""); + escapedMac.toLowerCase(); + + #ifdef ESPALEXA_ASYNC + serverAsync = externalServer; + #else + server = externalServer; + #endif + #ifdef ARDUINO_ARCH_ESP32 + udpConnected = espalexaUdp.beginMulticast(IPAddress(239, 255, 255, 250), 1900); + #else + udpConnected = espalexaUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 255, 255, 250), 1900); + #endif + + if (udpConnected){ + + startHttpServer(); + EA_DEBUGLN("Done"); + return true; + } + EA_DEBUGLN("Failed"); + return false; + } + + //service loop + void loop() { + #ifndef ESPALEXA_ASYNC + if (server == nullptr) return; //only if begin() was not called + server->handleClient(); + #endif + + if (!udpConnected) return; + int packetSize = espalexaUdp.parsePacket(); + if (!packetSize) return; //no new udp packet + + EA_DEBUGLN("Got UDP!"); + int len = espalexaUdp.read(packetBuffer, 254); + if (len > 0) { + packetBuffer[len] = 0; + } + + String request = packetBuffer; + EA_DEBUGLN(request); + if(request.indexOf("M-SEARCH") >= 0) { + if(request.indexOf("upnp:rootdevice") > 0 || request.indexOf("asic:1") > 0) { + EA_DEBUGLN("Responding search req..."); + respondToSearch(); + } + } + } + + bool addDevice(EspalexaDevice* d) + { + EA_DEBUG("Adding device "); + EA_DEBUGLN((currentDeviceCount+1)); + if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return false; + devices[currentDeviceCount] = d; + currentDeviceCount++; + return true; + } + + bool addDevice(String deviceName, CallbackBriFunction callback, uint8_t initialValue = 0) + { + EA_DEBUG("Constructing device "); + EA_DEBUGLN((currentDeviceCount+1)); + if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return false; + EspalexaDevice* d = new EspalexaDevice(deviceName, callback, initialValue); + return addDevice(d); + } + + bool addDevice(String deviceName, CallbackColFunction callback, uint8_t initialValue = 0) + { + EA_DEBUG("Constructing device "); + EA_DEBUGLN((currentDeviceCount+1)); + if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return false; + EspalexaDevice* d = new EspalexaDevice(deviceName, callback, initialValue); + return addDevice(d); + } + + //basic implementation of Philips hue api functions needed for basic Alexa control + #ifdef ESPALEXA_ASYNC + bool handleAlexaApiCall(AsyncWebServerRequest* request) + { + server = request; //copy request reference + String req = request->url(); //body from global variable + EA_DEBUGLN(request->contentType()); + if (request->hasParam("body", true)) // This is necessary, otherwise ESP crashes if there is no body + { + EA_DEBUG("BodyMethod2"); + body = request->getParam("body", true)->value(); + } + EA_DEBUG("FinalBody: "); + EA_DEBUGLN(body); + #else + bool handleAlexaApiCall(String req, String body) + { + #endif + EA_DEBUGLN("AlexaApiCall"); + if (req.indexOf("api") <0) return false; //return if not an API call + EA_DEBUGLN("ok"); + + if (body.indexOf("devicetype") > 0) //client wants a hue api username, we dont care and give static + { + EA_DEBUGLN("devType"); + body = ""; + server->send(200, "application/json", "[{\"success\":{\"username\": \"2WLEDHardQrI3WHYTHoMcXHgEspsM8ZZRpSKtBQr\"}}]"); + return true; + } + + if (req.indexOf("state") > 0) //client wants to control light + { + server->send(200, "application/json", "[{\"success\":true}]"); //short valid response + + int tempDeviceId = req.substring(req.indexOf("lights")+7).toInt(); + EA_DEBUG("ls"); EA_DEBUGLN(tempDeviceId); + if (body.indexOf("false")>0) {alexaOff(tempDeviceId); return true;} + if (body.indexOf("bri")>0 ) {alexaDim(tempDeviceId, body.substring(body.indexOf("bri") +5).toInt()); return true;} + if (body.indexOf("hue")>0 ) {alexaCol(tempDeviceId, body.substring(body.indexOf("hue") +5).toInt(), body.substring(body.indexOf("sat") +5).toInt()); return true;} + if (body.indexOf("ct") >0 ) {alexaCt (tempDeviceId, body.substring(body.indexOf("ct") +4).toInt()); return true;} + alexaOn(tempDeviceId); + + return true; + } + + int pos = req.indexOf("lights"); + if (pos > 0) //client wants light info + { + int tempDeviceId = req.substring(pos+7).toInt(); + EA_DEBUG("l"); EA_DEBUGLN(tempDeviceId); + + if (tempDeviceId == 0) //client wants all lights + { + EA_DEBUGLN("lAll"); + String jsonTemp = "{"; + for (int i = 0; isend(200, "application/json", jsonTemp); + } else //client wants one light (tempDeviceId) + { + server->send(200, "application/json", deviceJsonString(tempDeviceId)); + } + + return true; + } + + //we dont care about other api commands at this time and send empty JSON + server->send(200, "application/json", "{}"); + return true; + } + + //is an unique device ID + String getEscapedMac() + { + return escapedMac; + } + + //convert brightness (0-255) to percentage + uint8_t toPercent(uint8_t bri) + { + uint16_t perc = bri * 100; + return perc / 255; + } + + ~Espalexa(){delete devices;} //note: Espalexa is NOT meant to be destructed +}; + +#endif + diff --git a/wled00/src/dependencies/espalexa/EspalexaDevice.cpp b/wled00/src/dependencies/espalexa/EspalexaDevice.cpp new file mode 100644 index 00000000..70f5cdb2 --- /dev/null +++ b/wled00/src/dependencies/espalexa/EspalexaDevice.cpp @@ -0,0 +1,176 @@ +//EspalexaDevice Class + +#include "EspalexaDevice.h" + +EspalexaDevice::EspalexaDevice(){} + +EspalexaDevice::EspalexaDevice(String deviceName, CallbackBriFunction gnCallback, uint8_t initialValue) { //constructor + + _deviceName = deviceName; + _callback = gnCallback; + _val = initialValue; + _val_last = _val; +} + +EspalexaDevice::EspalexaDevice(String deviceName, CallbackColFunction gnCallback, uint8_t initialValue) { //constructor for color device + + _deviceName = deviceName; + _callbackCol = gnCallback; + _callback = nullptr; + _val = initialValue; + _val_last = _val; +} + +EspalexaDevice::~EspalexaDevice(){/*nothing to destruct*/} + +bool EspalexaDevice::isColorDevice() +{ + //if brightness-only callback is null, we have color device + return (_callback == nullptr); +} + +bool EspalexaDevice::isColorTemperatureMode() +{ + return _ct; +} + +String EspalexaDevice::getName() +{ + return _deviceName; +} + +uint8_t EspalexaDevice::getLastChangedProperty() +{ + return _changed; +} + +uint8_t EspalexaDevice::getValue() +{ + return _val; +} + +uint16_t EspalexaDevice::getHue() +{ + return _hue; +} + +uint8_t EspalexaDevice::getSat() +{ + return _sat; +} + +uint16_t EspalexaDevice::getCt() +{ + if (_ct == 0) return 500; + return _ct; +} + +uint32_t EspalexaDevice::getColorRGB() +{ + uint8_t rgb[3]; + + if (isColorTemperatureMode()) + { + //TODO tweak a bit to match hue lamp characteristics + //based on https://gist.github.com/paulkaplan/5184275 + float temp = 10000/ _ct; //kelvins = 1,000,000/mired (and that /100) + float r, g, b; + + if( temp <= 66 ){ + r = 255; + g = temp; + g = 99.470802 * log(g) - 161.119568; + if( temp <= 19){ + b = 0; + } else { + b = temp-10; + b = 138.517731 * log(b) - 305.044793; + } + } else { + r = temp - 60; + r = 329.698727 * pow(r, -0.13320476); + g = temp - 60; + g = 288.12217 * pow(g, -0.07551485 ); + b = 255; + } + + rgb[0] = (byte)constrain(r,0.1,255.1); + rgb[1] = (byte)constrain(g,0.1,255.1); + rgb[2] = (byte)constrain(b,0.1,255.1); + } + else + { //hue + sat mode + float h = ((float)_hue)/65535.0; + float s = ((float)_sat)/255.0; + byte i = floor(h*6); + float f = h * 6-i; + float p = 255 * (1-s); + float q = 255 * (1-f*s); + float t = 255 * (1-(1-f)*s); + switch (i%6) { + case 0: rgb[0]=255,rgb[1]=t,rgb[2]=p;break; + case 1: rgb[0]=q,rgb[1]=255,rgb[2]=p;break; + case 2: rgb[0]=p,rgb[1]=255,rgb[2]=t;break; + case 3: rgb[0]=p,rgb[1]=q,rgb[2]=255;break; + case 4: rgb[0]=t,rgb[1]=p,rgb[2]=255;break; + case 5: rgb[0]=255,rgb[1]=p,rgb[2]=q; + } + } + return ((rgb[0] << 16) | (rgb[1] << 8) | (rgb[2])); +} + +uint8_t EspalexaDevice::getLastValue() +{ + if (_val_last == 0) return 255; + return _val_last; +} + +void EspalexaDevice::setPropertyChanged(uint8_t p) +{ + //0: initial 1: on 2: off 3: bri 4: col 5: ct + _changed = p; +} + +//you need to re-discover the device for the Alexa name to change +void EspalexaDevice::setName(String name) +{ + _deviceName = name; +} + +void EspalexaDevice::setValue(uint8_t val) +{ + if (_val != 0) + { + _val_last = _val; + } + if (val != 0) + { + _val_last = val; + } + _val = val; +} + +void EspalexaDevice::setPercent(uint8_t perc) +{ + uint16_t val = perc * 255; + val /= 100; + if (val > 255) val = 255; + setValue(val); +} + +void EspalexaDevice::setColor(uint16_t hue, uint8_t sat) +{ + _hue = hue; + _sat = sat; + _ct = 0; +} + +void EspalexaDevice::setColor(uint16_t ct) +{ + _ct = ct; +} + +void EspalexaDevice::doCallback() +{ + (_callback != nullptr) ? _callback(_val) : _callbackCol(_val, getColorRGB()); +} \ No newline at end of file diff --git a/wled00/src/dependencies/espalexa/EspalexaDevice.h b/wled00/src/dependencies/espalexa/EspalexaDevice.h new file mode 100644 index 00000000..0974ff3a --- /dev/null +++ b/wled00/src/dependencies/espalexa/EspalexaDevice.h @@ -0,0 +1,46 @@ +#ifndef EspalexaDevice_h +#define EspalexaDevice_h + +#include "Arduino.h" + +typedef void (*CallbackBriFunction) (uint8_t br); +typedef void (*CallbackColFunction) (uint8_t br, uint32_t col); + +class EspalexaDevice { +private: + String _deviceName; + CallbackBriFunction _callback; + CallbackColFunction _callbackCol; + uint8_t _val, _val_last, _sat = 0; + uint16_t _hue = 0, _ct = 0; + uint8_t _changed = 0; + +public: + EspalexaDevice(); + ~EspalexaDevice(); + EspalexaDevice(String deviceName, CallbackBriFunction gnCallback, uint8_t initialValue =0); + EspalexaDevice(String deviceName, CallbackColFunction gnCallback, uint8_t initialValue =0); + + bool isColorDevice(); + bool isColorTemperatureMode(); + String getName(); + uint8_t getLastChangedProperty(); + uint8_t getValue(); + uint16_t getHue(); + uint8_t getSat(); + uint16_t getCt(); + uint32_t getColorRGB(); + + void setPropertyChanged(uint8_t p); + void setValue(uint8_t bri); + void setPercent(uint8_t perc); + void setName(String name); + void setColor(uint16_t hue, uint8_t sat); + void setColor(uint16_t ct); + + void doCallback(); + + uint8_t getLastValue(); //last value that was not off (1-255) +}; + +#endif \ No newline at end of file diff --git a/wled00/src/dependencies/espalexa/LICENSE b/wled00/src/dependencies/espalexa/LICENSE new file mode 100644 index 00000000..37f4d769 --- /dev/null +++ b/wled00/src/dependencies/espalexa/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Christian Schwinne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/wled00/src/dependencies/timezone/Timezone.h b/wled00/src/dependencies/timezone/Timezone.h index c55d0c46..991eca0a 100644 --- a/wled00/src/dependencies/timezone/Timezone.h +++ b/wled00/src/dependencies/timezone/Timezone.h @@ -16,7 +16,7 @@ #else #include #endif -#include //http://www.arduino.cc/playground/Code/Time +#include "../time/Time.h" //http://www.arduino.cc/playground/Code/Time //convenient constants for dstRules enum week_t {Last, First, Second, Third, Fourth}; diff --git a/wled00/wled00.ino b/wled00/wled00.ino index 37bfec49..8abb527a 100644 --- a/wled00/wled00.ino +++ b/wled00/wled00.ino @@ -3,7 +3,7 @@ */ /* * @title WLED project sketch - * @version 0.8.2 + * @version 0.8.3 * @author Christian Schwinne */ @@ -60,6 +60,10 @@ #include "src/dependencies/time/Time.h" #include "src/dependencies/time/TimeLib.h" #include "src/dependencies/timezone/Timezone.h" +#ifndef WLED_DISABLE_ALEXA + #define ESPALEXA_MAXDEVICES 1 + #include "src/dependencies/espalexa/Espalexa.h" +#endif #ifndef WLED_DISABLE_BLYNK #include "src/dependencies/blynk/BlynkSimpleEsp.h" #endif @@ -74,8 +78,8 @@ //version code in format yymmddb (b = daily build) -#define VERSION 1812141 -char versionString[] = "0.8.2"; +#define VERSION 1901091 +char versionString[] = "0.8.3-dev"; //AP and OTA default passwords (for maximum change them!) @@ -370,11 +374,11 @@ unsigned long auxStartTime = 0; bool auxActive = false, auxActiveBefore = false; //alexa udp -WiFiUDP alexaUDP; -bool alexaUdpConnected = false; -IPAddress ipMulti(239, 255, 255, 250); -unsigned int portMulti = 1900; String escapedMac; +#ifndef WLED_DISABLE_ALEXA +Espalexa espalexa; +EspalexaDevice* espalexaDevice; +#endif //dns server DNSServer dnsServer; @@ -465,6 +469,7 @@ void serveMessage(int,String,String,int=255); void reset() { briT = 0; + delay(250); //enough time to send response to client setAllLeds(); DEBUG_PRINTLN("MODULE RESET"); ESP.restart(); diff --git a/wled00/wled01_eeprom.ino b/wled00/wled01_eeprom.ino index 4927433e..90552bcb 100644 --- a/wled00/wled01_eeprom.ino +++ b/wled00/wled01_eeprom.ino @@ -461,7 +461,7 @@ void loadSettingsFromEEPROM(bool first) strip.colorOrder = EEPROM.read(383); irEnabled = EEPROM.read(385); strip.ablMilliampsMax = EEPROM.read(387) + ((EEPROM.read(388) << 8) & 0xFF00); - } else if (lastEEPROMversion > 1) //ABL is off by default when updating from version older than 0.8.2 + } else if (lastEEPROMversion > 1) //ABL is off by default when updating from version older than 0.8.3 { strip.ablMilliampsMax = 65000; } else { diff --git a/wled00/wled03_set.ino b/wled00/wled03_set.ino index 175796ad..df94d563 100644 --- a/wled00/wled03_set.ino +++ b/wled00/wled03_set.ino @@ -309,6 +309,7 @@ void handleSettingsSet(byte subPage) } saveSettingsToEEPROM(); if (subPage == 2) strip.init(useRGBW,ledCount,skipFirstLed); + if (subPage == 4) alexaInit(); } diff --git a/wled00/wled05_init.ino b/wled00/wled05_init.ino index ab98c7a6..127a392a 100644 --- a/wled00/wled05_init.ino +++ b/wled00/wled05_init.ino @@ -271,7 +271,7 @@ void getBuildInfo() oappend("\r\nstrip-pin: gpio"); oappendi(LEDPIN); oappend("\r\nbrand: wled"); - oappend("\r\nbuild-type: src\r\n"); + oappend("\r\nbuild-type: dev\r\n"); } diff --git a/wled00/wled07_notify.ino b/wled00/wled07_notify.ino index 958f8a59..6befaae5 100644 --- a/wled00/wled07_notify.ino +++ b/wled00/wled07_notify.ino @@ -16,9 +16,10 @@ void notify(byte callMode, bool followUp=false) case 2: if (!notifyButton) return; break; case 4: if (!notifyDirect) return; break; case 6: if (!notifyDirect) return; break; //fx change - case 7: if (!notifyHue) return; break; + case 7: if (!notifyHue) return; break; case 8: if (!notifyDirect) return; break; case 9: if (!notifyDirect) return; break; + case 10: if (!notifyAlexa) return; break; default: return; } byte udpOut[WLEDPACKETSIZE]; diff --git a/wled00/wled08_led.ino b/wled00/wled08_led.ino index ffda2762..65396161 100644 --- a/wled00/wled08_led.ino +++ b/wled00/wled08_led.ino @@ -36,7 +36,7 @@ void setAllLeds() { } whiteSecT = whiteSec; } - if (autoRGBtoRGBW) + if (useRGBW && autoRGBtoRGBW) { colorRGBtoRGBW(colT,&whiteT); colorRGBtoRGBW(colSecT,&whiteSecT); @@ -87,7 +87,7 @@ bool colorChanged() void colorUpdated(int callMode) { //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) - // 6: fx changed 7: hue 8: preset cycle 9: blynk + // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa bool fxChanged = strip.setEffectConfig(effectCurrent, effectSpeed, effectIntensity, effectPalette); if (!colorChanged()) { @@ -146,6 +146,9 @@ void colorUpdated(int callMode) } if (callMode == 8) return; + #ifndef WLED_DISABLE_ALEXA + if (espalexaDevice != nullptr) espalexaDevice->setValue(bri); + #endif //only update Blynk and mqtt every 2 seconds to reduce lag if (millis() - lastInterfaceUpdate <= 2000) { diff --git a/wled00/wled12_alexa.ino b/wled00/wled12_alexa.ino index fe703c93..f9ccb6a5 100644 --- a/wled00/wled12_alexa.ino +++ b/wled00/wled12_alexa.ino @@ -12,280 +12,69 @@ void prepareIds() { } #ifndef WLED_DISABLE_ALEXA +void onAlexaChange(byte b, uint32_t color); + void alexaInit() { if (alexaEnabled && WiFi.status() == WL_CONNECTED) { - alexaUdpConnected = connectUDP(); - - if (alexaUdpConnected) alexaInitPages(); + if (espalexaDevice == nullptr) //only init once + { + espalexaDevice = new EspalexaDevice(alexaInvocationName, onAlexaChange); + espalexa.addDevice(espalexaDevice); + espalexa.begin(&server); + } else { + espalexaDevice->setName(alexaInvocationName); + } } } void handleAlexa() { - if (!alexaEnabled || WiFi.status() != WL_CONNECTED || !alexaUdpConnected) return; - - // if there's data available, read a packet - int packetSize = alexaUDP.parsePacket(); - if(packetSize < 1) return; - - IPAddress remote = alexaUDP.remoteIP(); - int len = alexaUDP.read(obuf, 254); - if (len > 0) obuf[len] = 0; - - if(strstr(obuf,"M-SEARCH") > 0) { - if(strstr(obuf,"upnp:rootdevice") > 0 || strstr(obuf,"device:basic:1") > 0) { - DEBUG_PRINTLN("Responding search req..."); - respondToSearch(); - } - } + if (!alexaEnabled || WiFi.status() != WL_CONNECTED) return; + espalexa.loop(); } -void alexaOn() +void onAlexaChange(byte b, uint32_t color) { - if (macroAlexaOn == 0) - { - handleSet((notifyAlexa)?"win&T=1&IN":"win&T=1&NN&IN"); - } else - { - applyMacro(macroAlexaOn); - } - - server.send(200, "application/json", "[{\"success\":{\"/lights/1/state/on\":true}}]"); -} - -void alexaOff() -{ - if (macroAlexaOff == 0) - { - handleSet((notifyAlexa)?"win&T=0&IN":"win&T=0&NN&IN"); - } else - { - applyMacro(macroAlexaOff); - } - - server.send(200, "application/json", "[{\"success\":{\"/lights/1/state/on\":false}}]"); -} - -void alexaDim(byte briL) -{ - olen = 0; - oappend("[{\"success\":{\"/lights/1/state/bri\":"); - oappendi(briL); - oappend("}}]"); - - server.send(200, "application/json", obuf); + byte m = espalexaDevice->getLastChangedProperty(); - String ct = (notifyAlexa)?"win&IN&A=":"win&NN&IN&A="; - if (briL < 255) + if (m == 1){ //ON + if (!macroAlexaOn) + { + if (bri == 0) + { + bri = briLast; + colorUpdated(10); + } + } else applyMacro(macroAlexaOn); + } else if (m == 2) //OFF { - ct = ct + (briL+1); - } else + if (!macroAlexaOff) + { + if (bri > 0) + { + briLast = bri; + bri = 0; + colorUpdated(10); + } + } else applyMacro(macroAlexaOff); + } else if (m == 3) //brightness { - ct = ct + (255); + bri = b; + colorUpdated(10); + } else //color + { + col[0] = ((color >> 16) & 0xFF); + col[1] = ((color >> 8) & 0xFF); + col[2] = (color & 0xFF); + if (useRGBW) colorRGBtoRGBW(col,&white); + colorUpdated(10); } - handleSet(ct); } -void respondToSearch() { - DEBUG_PRINTLN(""); - DEBUG_PRINT("Send resp to "); - DEBUG_PRINTLN(alexaUDP.remoteIP()); - DEBUG_PRINT("Port : "); - DEBUG_PRINTLN(alexaUDP.remotePort()); - - IPAddress localIP = WiFi.localIP(); - char s[16]; - sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); - - olen = 0; - oappend( - "HTTP/1.1 200 OK\r\n" - "EXT:\r\n" - "CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL - "LOCATION: http://"); - oappend(s); - oappend(":80/description.xml\r\n" - "SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber - "hue-bridgeid: "); - oappend((char*)escapedMac.c_str()); - oappend("\r\n" - "ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType - "USN: uuid:2f402f80-da50-11e1-9b23-"); - oappend((char*)escapedMac.c_str()); - oappend("::upnp:rootdevice\r\n" // _uuid::_deviceType - "\r\n"); - - alexaUDP.beginPacket(alexaUDP.remoteIP(), alexaUDP.remotePort()); - #ifdef ARDUINO_ARCH_ESP32 - alexaUDP.write((byte*)obuf, olen); - #else - alexaUDP.write(obuf); - #endif - alexaUDP.endPacket(); - - DEBUG_PRINTLN("Response sent!"); -} - -void alexaInitPages() { - - server.on("/description.xml", HTTP_GET, [](){ - DEBUG_PRINTLN(" # Responding to description.xml ... #\n"); - - IPAddress localIP = WiFi.localIP(); - char s[16]; - sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); - - olen = 0; - oappend("" - "" - "10" - "http://"); - oappend(s); - oappend(":80/" - "" - "urn:schemas-upnp-org:device:Basic:1" - "Philips hue ("); - oappend(s); - oappend(")" - "Royal Philips Electronics" - "http://www.philips.com" - "Philips hue Personal Wireless Lighting" - "Philips hue bridge 2012" - "929000226503" - "http://www.meethue.com" - ""); - oappend((char*)escapedMac.c_str()); - oappend("" - "uuid:2f402f80-da50-11e1-9b23-"); - oappend((char*)escapedMac.c_str()); - oappend("" - "index.html" - "" - " " - " image/png" - " 48" - " 48" - " 24" - " hue_logo_0.png" - " " - " " - " image/png" - " 120" - " 120" - " 24" - " hue_logo_3.png" - " " - "" - "" - ""); - - server.send(200, "text/xml", obuf); - - DEBUG_PRINTLN("Sending setup_xml"); - }); - - // openHAB support - server.on("/on.html", HTTP_GET, [](){ - DEBUG_PRINTLN("on req"); - server.send(200, "text/plain", "turned on"); - alexaOn(); - }); - - server.on("/off.html", HTTP_GET, [](){ - DEBUG_PRINTLN("off req"); - server.send(200, "text/plain", "turned off"); - alexaOff(); - }); - - server.on("/status.html", HTTP_GET, [](){ - DEBUG_PRINTLN("Got status request"); - - char statrespone[] = "0"; - if (bri > 0) { - statrespone[0] = '1'; - } - server.send(200, "text/plain", statrespone); - }); -} - -String boolString(bool st) -{ - return (st)?"true":"false"; -} - -String briForHue(int realBri) -{ - realBri--; - if (realBri < 0) realBri = 0; - return String(realBri); -} - -bool handleAlexaApiCall(String req, String body) //basic implementation of Philips hue api functions needed for basic Alexa control -{ - DEBUG_PRINTLN("AlexaApiCall"); - if (req.indexOf("api") <0) return false; - DEBUG_PRINTLN("ok"); - if (body.indexOf("devicetype") > 0) //client wants a hue api username, we dont care and give static - { - DEBUG_PRINTLN("devType"); - server.send(200, "application/json", "[{\"success\":{\"username\": \"2WLEDHardQrI3WHYTHoMcXHgEspsM8ZZRpSKtBQr\"}}]"); - return true; - } - if (req.indexOf("state") > 0) //client wants to control light - { - DEBUG_PRINTLN("ls"); - if (body.indexOf("bri")>0) {alexaDim(body.substring(body.indexOf("bri") +5).toInt()); return true;} - if (body.indexOf("false")>0) {alexaOff(); return true;} - alexaOn(); - - return true; - } - if (req.indexOf("lights/1") > 0) //client wants light info - { - DEBUG_PRINTLN("l1"); - server.send(200, "application/json", "{\"manufacturername\":\"OpenSource\",\"modelid\":\"LST001\",\"name\":\""+ String(alexaInvocationName) +"\",\"state\":{\"on\":"+ boolString(bri) +",\"hue\":0,\"bri\":"+ briForHue(bri) +",\"sat\":0,\"xy\":[0.00000,0.00000],\"ct\":500,\"alert\":\"none\",\"effect\":\"none\",\"colormode\":\"hs\",\"reachable\":true},\"swversion\":\"0.1\",\"type\":\"Extended color light\",\"uniqueid\":\"2\"}"); - - return true; - } - if (req.indexOf("lights") > 0) //client wants all lights - { - DEBUG_PRINTLN("lAll"); - server.send(200, "application/json", "{\"1\":{\"type\":\"Extended color light\",\"manufacturername\":\"OpenSource\",\"swversion\":\"0.1\",\"name\":\""+ String(alexaInvocationName) +"\",\"uniqueid\":\""+ WiFi.macAddress() +"-2\",\"modelid\":\"LST001\",\"state\":{\"on\":"+ boolString(bri) +",\"bri\":"+ briForHue(bri) +",\"xy\":[0.00000,0.00000],\"colormode\":\"hs\",\"effect\":\"none\",\"ct\":500,\"hue\":0,\"sat\":0,\"alert\":\"none\",\"reachable\":true}}}"); - return true; - } - - //we dont care about other api commands at this time and send empty JSON - server.send(200, "application/json", "{}"); - return true; -} - -bool connectUDP(){ - bool state = false; - - DEBUG_PRINTLN(""); - DEBUG_PRINTLN("Con UDP"); - - #ifdef ARDUINO_ARCH_ESP32 - if(alexaUDP.beginMulticast(ipMulti, portMulti)) - #else - if(alexaUDP.beginMulticast(WiFi.localIP(), ipMulti, portMulti)) - #endif - { - DEBUG_PRINTLN("Con success"); - state = true; - } - else{ - DEBUG_PRINTLN("Con failed"); - } - - return state; -} #else void alexaInit(){} void handleAlexa(){} - void alexaInitPages(){} - bool handleAlexaApiCall(String req, String body){return false;} #endif diff --git a/wled00/wled18_server.ino b/wled00/wled18_server.ino index 0d4a0929..8c990fc8 100644 --- a/wled00/wled18_server.ino +++ b/wled00/wled18_server.ino @@ -178,7 +178,9 @@ void initServer() } if(!handleSet(server.uri())){ - if(!handleAlexaApiCall(server.uri(),server.arg(0))) + #ifndef WLED_DISABLE_ALEXA + if(!espalexa.handleAlexaApiCall(server.uri(),server.arg(0))) + #endif server.send(404, "text/plain", "Not Found"); } });