Added Alexa Color support

This commit is contained in:
cschwinne 2019-01-09 22:52:42 +01:00
parent bec745d095
commit 5d1993935e
18 changed files with 833 additions and 289 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -437,7 +437,7 @@ HTTP traffic is unencrypted. An attacker in the same network can intercept form
<button type="button" onclick="U()">Manual OTA Update</button><br>
Enable ArduinoOTA: <input type="checkbox" name="AO"><br>
<h3>About</h3>
<a href="https://github.com/Aircoookie/WLED" target="_blank">WLED</a> version 0.8.2<br><br>
<a href="https://github.com/Aircoookie/WLED" target="_blank">WLED</a> version 0.8.3<br><br>
<a href="https://github.com/Aircoookie/WLED/wiki/Contributors-&-About" target="_blank">Contributors, dependencies and special thanks</a><br>
A huge thank you to everyone who helped me create WLED!<br><br>
(c) 2016-2018 Christian Schwinne <br>

View File

@ -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 <Espalexa.h>
//#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 <AsyncTCP.h>
#else
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>
#else
#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#include <WebServer.h> //if you get an error here please update to ESP32 arduino core 1.0.0
#else
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>
#endif
#endif
#include <WiFiUdp.h>
#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; i<currentDeviceCount; i++)
{
res += "Value of device " + String(i+1) + " (" + devices[i]->getName() + "): " + 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 = "<?xml version=\"1.0\" ?>"
"<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
"<specVersion><major>1</major><minor>0</minor></specVersion>"
"<URLBase>http://"+ String(s) +":80/</URLBase>"
"<device>"
"<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>"
"<friendlyName>Philips hue ("+ String(s) +")</friendlyName>"
"<manufacturer>Royal Philips Electronics</manufacturer>"
"<manufacturerURL>http://www.philips.com</manufacturerURL>"
"<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>"
"<modelName>Philips hue bridge 2012</modelName>"
"<modelNumber>929000226503</modelNumber>"
"<modelURL>http://www.meethue.com</modelURL>"
"<serialNumber>"+ escapedMac +"</serialNumber>"
"<UDN>uuid:2f402f80-da50-11e1-9b23-"+ escapedMac +"</UDN>"
"<presentationURL>index.html</presentationURL>"
"<iconList>"
" <icon>"
" <mimetype>image/png</mimetype>"
" <height>48</height>"
" <width>48</width>"
" <depth>24</depth>"
" <url>hue_logo_0.png</url>"
" </icon>"
" <icon>"
" <mimetype>image/png</mimetype>"
" <height>120</height>"
" <width>120</width>"
" <depth>24</depth>"
" <url>hue_logo_3.png</url>"
" </icon>"
"</iconList>"
"</device>"
"</root>";
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; i<currentDeviceCount; i++)
{
jsonTemp += "\"" + String(i+1) + "\":";
jsonTemp += deviceJsonString(i+1);
if (i < currentDeviceCount-1) jsonTemp += ",";
}
jsonTemp += "}";
server->send(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

View File

@ -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());
}

View File

@ -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

View File

@ -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.

View File

@ -16,7 +16,7 @@
#else
#include <WProgram.h>
#endif
#include <Time.h> //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};

View File

@ -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();

View File

@ -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 {

View File

@ -309,6 +309,7 @@ void handleSettingsSet(byte subPage)
}
saveSettingsToEEPROM();
if (subPage == 2) strip.init(useRGBW,ledCount,skipFirstLed);
if (subPage == 4) alexaInit();
}

View File

@ -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");
}

View File

@ -19,6 +19,7 @@ void notify(byte callMode, bool followUp=false)
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];

View File

@ -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)
{

View File

@ -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)
byte m = espalexaDevice->getLastChangedProperty();
if (m == 1){ //ON
if (!macroAlexaOn)
{
handleSet((notifyAlexa)?"win&T=1&IN":"win&T=1&NN&IN");
} else
if (bri == 0)
{
applyMacro(macroAlexaOn);
bri = briLast;
colorUpdated(10);
}
server.send(200, "application/json", "[{\"success\":{\"/lights/1/state/on\":true}}]");
}
void alexaOff()
} else applyMacro(macroAlexaOn);
} else if (m == 2) //OFF
{
if (macroAlexaOff == 0)
if (!macroAlexaOff)
{
handleSet((notifyAlexa)?"win&T=0&IN":"win&T=0&NN&IN");
} else
if (bri > 0)
{
applyMacro(macroAlexaOff);
briLast = bri;
bri = 0;
colorUpdated(10);
}
server.send(200, "application/json", "[{\"success\":{\"/lights/1/state/on\":false}}]");
}
void alexaDim(byte briL)
} else applyMacro(macroAlexaOff);
} else if (m == 3) //brightness
{
olen = 0;
oappend("[{\"success\":{\"/lights/1/state/bri\":");
oappendi(briL);
oappend("}}]");
server.send(200, "application/json", obuf);
String ct = (notifyAlexa)?"win&IN&A=":"win&NN&IN&A=";
if (briL < 255)
bri = b;
colorUpdated(10);
} else //color
{
ct = ct + (briL+1);
} else
{
ct = ct + (255);
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("<?xml version=\"1.0\" ?>"
"<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
"<specVersion><major>1</major><minor>0</minor></specVersion>"
"<URLBase>http://");
oappend(s);
oappend(":80/</URLBase>"
"<device>"
"<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>"
"<friendlyName>Philips hue (");
oappend(s);
oappend(")</friendlyName>"
"<manufacturer>Royal Philips Electronics</manufacturer>"
"<manufacturerURL>http://www.philips.com</manufacturerURL>"
"<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>"
"<modelName>Philips hue bridge 2012</modelName>"
"<modelNumber>929000226503</modelNumber>"
"<modelURL>http://www.meethue.com</modelURL>"
"<serialNumber>");
oappend((char*)escapedMac.c_str());
oappend("</serialNumber>"
"<UDN>uuid:2f402f80-da50-11e1-9b23-");
oappend((char*)escapedMac.c_str());
oappend("</UDN>"
"<presentationURL>index.html</presentationURL>"
"<iconList>"
" <icon>"
" <mimetype>image/png</mimetype>"
" <height>48</height>"
" <width>48</width>"
" <depth>24</depth>"
" <url>hue_logo_0.png</url>"
" </icon>"
" <icon>"
" <mimetype>image/png</mimetype>"
" <height>120</height>"
" <width>120</width>"
" <depth>24</depth>"
" <url>hue_logo_3.png</url>"
" </icon>"
"</iconList>"
"</device>"
"</root>");
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

View File

@ -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");
}
});