diff --git a/examples/EspalexaColor/EspalexaColor.ino b/examples/EspalexaColor/EspalexaColor.ino deleted file mode 100644 index 3cfbf68..0000000 --- a/examples/EspalexaColor/EspalexaColor.ino +++ /dev/null @@ -1,96 +0,0 @@ -/* - * This is a basic example on how to use Espalexa with RGB color devices. - */ -#ifdef ARDUINO_ARCH_ESP32 -#include -#else -#include -#endif - -#include - -// prototypes -boolean connectWifi(); - -//callback function prototype -void colorLightChanged(uint8_t brightness, uint32_t rgb); - -// Change this!! -const char* ssid = "..."; -const char* password = "wifipassword"; - -boolean wifiConnected = false; - -Espalexa espalexa; - -void setup() -{ - Serial.begin(115200); - // Initialise wifi connection - wifiConnected = connectWifi(); - - if(wifiConnected){ - espalexa.addDevice("Light", colorLightChanged); - - espalexa.begin(); - - } else - { - while (1) { - Serial.println("Cannot connect to WiFi. Please check data and reset the ESP."); - delay(2500); - } - } -} - -void loop() -{ - espalexa.loop(); - delay(1); -} - -//the color device callback function has two parameters -void colorLightChanged(uint8_t brightness, uint32_t rgb) { - //do what you need to do here, for example control RGB LED strip - Serial.print("Brightness: "); - Serial.print(brightness); - Serial.print(", Red: "); - Serial.print((rgb >> 16) & 0xFF); //get red component - Serial.print(", Green: "); - Serial.print((rgb >> 8) & 0xFF); //get green - Serial.print(", Blue: "); - Serial.println(rgb & 0xFF); //get blue -} - -// connect to wifi – returns true if successful or false if not -boolean connectWifi(){ - boolean state = true; - int i = 0; - - WiFi.mode(WIFI_STA); - WiFi.begin(ssid, password); - Serial.println(""); - Serial.println("Connecting to WiFi"); - - // Wait for connection - Serial.print("Connecting..."); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - if (i > 40){ - state = false; break; - } - i++; - } - Serial.println(""); - if (state){ - Serial.print("Connected to "); - Serial.println(ssid); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - } - else { - Serial.println("Connection failed."); - } - return state; -} diff --git a/examples/EspalexaFullyFeatured/EspalexaFullyFeatured.ino b/examples/EspalexaFullyFeatured/EspalexaFullyFeatured.ino new file mode 100644 index 0000000..8e36ef7 --- /dev/null +++ b/examples/EspalexaFullyFeatured/EspalexaFullyFeatured.ino @@ -0,0 +1,183 @@ +/* + * An example that demonstrates most capabilities of Espalexa v2.4.0 + */ +#ifdef ARDUINO_ARCH_ESP32 +#include +#else +#include +#endif +//#define ESPALEXA_ASYNC //uncomment for async operation (can fix empty body issue) +//#define ESPALEXA_NO_SUBPAGE //disable /espalexa status page +//#define ESPALEXA_DEBUG //activate debug serial logging +//#define ESPALEXA_MAXDEVICES 15 //set maximum devices add-able to Espalexa +#include + +// Change this!! +const char* ssid = "..."; +const char* password = "wifipassword"; + +// prototypes +bool connectWifi(); + +//callback functions +//new callback type, contains device pointer +void alphaChanged(EspalexaDevice* dev); +void betaChanged(EspalexaDevice* dev); +void gammaChanged(EspalexaDevice* dev); +//you can now use one callback for multiple devices +void deltaOrEpsilonChanged(EspalexaDevice* dev); + +//create devices yourself +EspalexaDevice* epsilon; + +bool wifiConnected = false; + +Espalexa espalexa; + +void setup() +{ + Serial.begin(115200); + // Initialise wifi connection + wifiConnected = connectWifi(); + + if(!wifiConnected){ + while (1) { + Serial.println("Cannot connect to WiFi. Please check data and reset the ESP."); + delay(2500); + } + } + + // Define your devices here. + espalexa.addDevice("Alpha", alphaChanged, EspalexaDeviceType::onoff); //non-dimmable device + espalexa.addDevice("Beta", betaChanged, EspalexaDeviceType::dimmable, 127); //Dimmable device, optional 4th parameter is beginning state (here fully on) + espalexa.addDevice("Gamma", gammaChanged, EspalexaDeviceType::whitespectrum); //color temperature (white spectrum) device + espalexa.addDevice("Delta", deltaOrEpsilonChanged, EspalexaDeviceType::color); //color device + + epsilon = new EspalexaDevice("Epsilon", deltaOrEpsilonChanged, EspalexaDeviceType::extendedcolor); //color + color temperature + espalexa.addDevice(epsilon); + epsilon->setValue(128); //creating the device yourself allows you to e.g. update their state value at any time! + + epsilon->setColor(200); //color temperature in mireds + epsilon->setColor(255,160,0); //color in RGB + epsilon->setColor(14000,255); //color in Hue + Sat + epsilon->setColorXY(0.50,0.50); //color in XY + + EspalexaDevice* d = espalexa.getDevice(3); //this will get "Delta", the index is zero-based + d->setPercent(50); //set value "brightness" in percent + + espalexa.begin(); +} + +void loop() +{ + espalexa.loop(); + delay(1); +} + +//our callback functions +void alphaChanged(EspalexaDevice* d) { + if (d == nullptr) return; //this is good practice, but not required + + //do what you need to do here + //EXAMPLE + Serial.print("A changed to "); + if (d->getValue()){ + Serial.println("ON"); + } + else { + Serial.println("OFF"); + } +} + +void betaChanged(EspalexaDevice* d) { + if (d == nullptr) return; + + uint8_t brightness = d->getValue(); + uint8_t percent = d->getPercent(); + uint8_t degrees = d->getDegrees(); //for heaters, HVAC, ... + + Serial.print("B changed to "); + Serial.print(percent); + Serial.println("%"); +} + +void gammaChanged(EspalexaDevice* d) { + if (d == nullptr) return; + Serial.print("C changed to "); + Serial.print(d->getValue()); + Serial.print(", colortemp "); + Serial.print(d->getCt()); + Serial.print(" ("); + Serial.print(d->getKelvin()); //this is more common than the hue mired values + Serial.println("K)"); +} + +void deltaOrEpsilonChanged(EspalexaDevice* d) +{ + if (d == nullptr) return; + + if (d->getId() == 3) //device "delta" + { + Serial.print("D changed to "); + Serial.print(d->getValue()); + Serial.print(", color R"); + Serial.print(d->getR()); + Serial.print(", G"); + Serial.print(d->getG()); + Serial.print(", B"); + Serial.println(d->getB()); + /*//alternative + uint32_t rgb = d->getRGB(); + Serial.print(", R "); Serial.print((rgb >> 16) & 0xFF); + Serial.print(", G "); Serial.print((rgb >> 8) & 0xFF); + Serial.print(", B "); Serial.println(rgb & 0xFF); */ + } else { //device "epsilon" + Serial.print("E changed to "); + Serial.print(d->getValue()); + Serial.print(", colormode "); + switch(d->getColorMode()) + { + case EspalexaColorMode::hs: + Serial.print("hs, "); Serial.print("hue "); Serial.print(d->getHue()); Serial.print(", sat "); Serial.println(d->getSat()); break; + case EspalexaColorMode::xy: + Serial.print("xy, "); Serial.print("x "); Serial.print(d->getX()); Serial.print(", y "); Serial.println(d->getY()); break; + case EspalexaColorMode::ct: + Serial.print("ct, "); Serial.print("ct "); Serial.println(d->getCt()); break; + case EspalexaColorMode::none: + Serial.println("none"); break; + } + } +} + +// connect to wifi – returns true if successful or false if not +bool connectWifi(){ + bool state = true; + int i = 0; + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + Serial.println(""); + Serial.println("Connecting to WiFi"); + + // Wait for connection + Serial.print("Connecting..."); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + if (i > 20){ + state = false; break; + } + i++; + } + Serial.println(""); + if (state){ + Serial.print("Connected to "); + Serial.println(ssid); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + } + else { + Serial.println("Connection failed."); + } + return state; +} diff --git a/keywords.txt b/keywords.txt index da96567..f80c6b3 100644 --- a/keywords.txt +++ b/keywords.txt @@ -7,4 +7,5 @@ ####################################### Espalexa KEYWORD1 -EspalexaDevice KEYWORD1 \ No newline at end of file +EspalexaDevice KEYWORD1 +EspalexaDeviceType KEYWORD1 \ No newline at end of file diff --git a/library.json b/library.json index 2d8379c..5b480f3 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "Espalexa", "frameworks": "Arduino", -"keywords": "alexa, esp32, esp8266, control, dimming", +"keywords": "alexa, esp32, esp8266, control, dimming, echo, color, async", "description": "Library to control an ESP module with the Alexa voice assistant", "url": "https://github.com/Aircoookie/Espalexa", "authors": @@ -15,5 +15,4 @@ "type": "git", "url": "https://github.com/Aircoookie/Espalexa" } -} - +} \ No newline at end of file diff --git a/library.properties b/library.properties index 04875d6..b6cd76a 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=Espalexa -version=2.3.4 +version=2.4.0 author=Christian Schwinne maintainer=Christian Schwinne sentence=Library to control an ESP module with the Alexa voice assistant diff --git a/readme.md b/readme.md index aa9c962..d02a05d 100644 --- a/readme.md +++ b/readme.md @@ -1,18 +1,20 @@ ## Espalexa allows you to easily control your ESP with the Alexa voice assistant. -It comes in an easy to use Arduino library or as a standalone example sketch. +It comes in an easy to use Arduino library. Now compatible with both ESP8266 and ESP32! -#### What does this do similar projects like Fauxmo don't already? +#### What does this do similar projects don't already? It allows you to set a ranged value (e.g. Brightness, Temperature) additionally to standard on/off control. For example, you can say "Alexa, turn the light to 75% / 21 degrees". Alexa now finally supports colors with the local API! You can see how to add color devices in the EspalexaColor example. -Then, you can say "Alexa, turn the light to Blue". Color temperature (white shades) is also supported, but still a WIP. +Then, you can say "Alexa, turn the light to Blue". Color temperature (white shades) is also supported. By default, it's possible to add up to a total of 10 devices (read below on how to increase the cap). Each device has a brightness range from 0 to 255, where 0 is off and 255 is fully on. You can get a percentage from that value using `espalexa.toPercent(brightness);` +FauxmoESP now also supports dimming! + #### How do I install the library? It's a standard Arduino library. Just download it and add it as ZIP library in the IDE. @@ -80,18 +82,35 @@ You can find a complete example implementation in the examples folder. Just chan Espalexa uses an internal WebServer. You can got to `http://[yourEspIP]/espalexa` to see all devices and their current state. +#### What devices types and Echos does Espalexa support? + +The library aims to work with every Echo on the market, but there are a few things to look out for. +Espalexa only works with a genuine Echo speaker, it probably wont work with Echo emulators, RPi homebrew devices or just the standalone app. +On an Echo Dot 1st and 2nd gen and the first gen Echo, color temperature adjustment (white spectrum) does not work as of March 2019. + +Here is an overview of the devices (light types) Espalexa can emulate: + +| Device type | Notes | +|------------------------------------------|-------------------------------------------------| +| EspalexaDeviceType::dimmable | Works as intended (dimming) | +| EspalexaDeviceType::whitespectrum | Color temperature adjustment not working on Dot | +| EspalexaDeviceType::color | Works as intended (dimming + color) | +| EspalexaDeviceType::extendedcolor | Color temperature adjustment not working on Dot | +| EspalexaDeviceType::onoff (experimental) | No control from app possible, only voice | + +See the example `EspalexaFullyFeatured` to learn how to define each device type and use the new EspalexaDevice pointer callback function type! + #### My devices are not found?! Confirm your ESP is connected. Go to the /espalexa subpage to confirm all your devices are defined. -Check your router configuration. Espalexa might need to have UPnP enabled for discovery to work. Then ask Alexa to discover devices again or try it via the Alexa app. If nothing helps, open a Github issue and we will help. If you can, add `#define ESPALEXA_DEBUG` before `#include ` and include the serial monitor output that is printed while the issue occurs. #### The devices are found but I can't control them! They are always on! -Please try using ESP8266 Arduino core version 2.3.0 or 2.5.0. -If you have to use 2.4.x, see this [workaround](https://github.com/Aircoookie/Espalexa/issues/6#issuecomment-366533897) or use the async server (below). +This is a known issue that occurs when using an Echo Dot (1st and 2nd gen). Please try using ESP8266 Arduino core version 2.3.0. +If you want to use a newer core, I recommend the async server mode (see example) or use this [workaround](https://github.com/Aircoookie/Espalexa/issues/6#issuecomment-366533897). #### I tried to use this in my sketch that already uses an ESP8266WebServer, it doesn't work! @@ -113,7 +132,7 @@ server.onNotFound([](){ #### Does this library work with ESPAsyncWebServer? -Yes! In v2.3.0 you can use the library asyncronously by adding `#define ESPALEXA_ASYNC` before `#include ` +Yes! From v2.3.0 you can use the library asyncronously by adding `#define ESPALEXA_ASYNC` before `#include ` See the `EspalexaWithAsyncWebServer` example. `ESPAsyncWebServer` and its dependencies must be manually installed. @@ -126,12 +145,6 @@ I recommend setting MAXDEVICES to the exact number of devices you want to add to #### How does this work? Espalexa emulates parts of the SSDP protocol and the Philips hue API, just enough so it can be discovered and controlled by Alexa. -This sketch is basically cobbled together from: +Parts of the code are based on: - [arduino-esp8266-alexa-wemo-switch](https://github.com/kakopappa/arduino-esp8266-alexa-wemo-switch) by kakopappa (Foundation) -- [ESP8266HueEmulator](https://github.com/probonopd/ESP8266HueEmulator) by probonopd -- Several hours of forensic SSDP and Hue protocol Wireshark work - -This is a more generalized version of the file wled12_alexa.ino in my main ESP lighting project [WLED](https://github.com/Aircoookie/WLED). - -Espalexa only works with a genuine Echo device, it probably wont work with Echo emulators or RPi homebrew devices. -You only need the src/dependencies folder if you compile for ESP32! \ No newline at end of file +- [ESP8266HueEmulator](https://github.com/probonopd/ESP8266HueEmulator) by probonopd \ No newline at end of file diff --git a/src/Espalexa.h b/src/Espalexa.h index 831b754..b55f3b2 100644 --- a/src/Espalexa.h +++ b/src/Espalexa.h @@ -10,7 +10,7 @@ */ /* * @title Espalexa library - * @version 2.3.4 + * @version 2.4.0 * @author Christian Schwinne * @license MIT * @contributors d-999 @@ -21,6 +21,9 @@ //you can use these defines for library config in your sketch. Just use them before #include //#define ESPALEXA_ASYNC +//in case this is unwanted in your application (will disable the /espalexa value page) +//#define ESPALEXA_NO_SUBPAGE + #ifndef ESPALEXA_MAXDEVICES #define ESPALEXA_MAXDEVICES 10 //this limit only has memory reasons, set it higher should you need to #endif @@ -46,7 +49,7 @@ #include #ifdef ESPALEXA_DEBUG - #pragma message "Espalexa 2.3.4 debug mode" + #pragma message "Espalexa 2.4.0 debug mode" #define EA_DEBUG(x) Serial.print (x) #define EA_DEBUGLN(x) Serial.println (x) #else @@ -56,6 +59,7 @@ #include "EspalexaDevice.h" + class Espalexa { private: //private member vars @@ -69,6 +73,7 @@ class Espalexa { ESP8266WebServer* server; #endif uint8_t currentDeviceCount = 0; + bool discoverable = true; EspalexaDevice* devices[ESPALEXA_MAXDEVICES] = {}; //Keep in mind that Device IDs go from 1 to DEVICES, cpp arrays from 0 to DEVICES-1!! @@ -80,44 +85,99 @@ class Espalexa { String escapedMac=""; //lowercase mac address //private member functions + String boolString(bool st) + { + return(st)?"true":"false"; + } + + String modeString(EspalexaColorMode m) + { + if (m == EspalexaColorMode::xy) return "xy"; + if (m == EspalexaColorMode::hs) return "hs"; + return "ct"; + } + + String typeString(EspalexaDeviceType t) + { + switch (t) + { + case EspalexaDeviceType::dimmable: return "Dimmable light"; + case EspalexaDeviceType::whitespectrum: return "Color temperature light"; + case EspalexaDeviceType::color: return "Color light"; + case EspalexaDeviceType::extendedcolor: return "Extended color light"; + } + return "Light"; + } + + String modelidString(EspalexaDeviceType t) + { + switch (t) + { + case EspalexaDeviceType::dimmable: return "LWB010"; + case EspalexaDeviceType::whitespectrum: return "LWT010"; + case EspalexaDeviceType::color: return "LST001"; + case EspalexaDeviceType::extendedcolor: return "LCT015"; + } + return "Plug 01"; + } + //device JSON string: color+temperature device emulates LCT015, dimmable device LWB010, (TODO: on/off Plug 01, color temperature device LWT010, color device LST001) 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\":\""; - json += dev->isColorDevice() ? "LCT015" : "LWB010"; - json += "\",\"state\":{\"on\":"; - json += boolString(dev->getValue()) +",\"bri\":"+ (String)(dev->getLastValue()-1) ; - if (dev->isColorDevice()) + + String json = "{\"state\":{\"on\":"; + json += boolString(dev->getValue()); + if (dev->getType() != EspalexaDeviceType::onoff) //bri support { - 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 += ",\"bri\":" + String(dev->getLastValue()-1); + if (static_cast(dev->getType()) > 2) //color support + { + json += ",\"hue\":" + String(dev->getHue()) + ",\"sat\":" + String(dev->getSat()); + json += ",\"effect\":\"none\",\"xy\":[" + String(dev->getX()) + "," + String(dev->getY()) + "]"; + } + if (static_cast(dev->getType()) > 1 && dev->getType() != EspalexaDeviceType::color) //white spectrum support + { + json += ",\"ct\":" + String(dev->getCt()); + } } - json +=",\"alert\":\"none\",\"reachable\":true}}"; + json += ",\"alert\":\"none"; + if (static_cast(dev->getType()) > 1) json += "\",\"colormode\":\"" + modeString(dev->getColorMode()); + json += "\",\"mode\":\"homeautomation\",\"reachable\":true},"; + json += "\"type\":\"" + typeString(dev->getType()); + json += "\",\"name\":\"" + dev->getName(); + json += "\",\"modelid\":\"" + modelidString(dev->getType()); + json += "\",\"manufacturername\":\"Espalexa\",\"productname\":\"E" + String(static_cast(dev->getType())); + json += "\",\"uniqueid\":\""+ WiFi.macAddress() +"-"+ (deviceId+1); + json += "\",\"swversion\":\"2.4.0\"}"; + return json; } //Espalexa status page /espalexa + #ifndef ESPALEXA_NO_SUBPAGE 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"; + EspalexaDevice* dev = devices[i]; + res += "Value of device " + String(i+1) + " (" + dev->getName() + "): " + String(dev->getValue()) + " (" + typeString(dev->getType()); + if (static_cast(dev->getType()) > 1) //color support + { + res += ", colormode=" + modeString(dev->getColorMode()) + ", r=" + String(dev->getR()) + ", g=" + String(dev->getG()) + ", b=" + String(dev->getB()); + res +=", ct=" + String(dev->getCt()) + ", hue=" + String(dev->getHue()) + ", sat=" + String(dev->getSat()) + ", x=" + String(dev->getX()) + ", y=" + String(dev->getY()); + } + res += ")\r\n"; } res += "\r\nFree Heap: " + (String)ESP.getFreeHeap(); res += "\r\nUptime: " + (String)millis(); - res += "\r\n\r\nEspalexa library v2.3.4 by Christian Schwinne 2019"; + res += "\r\n\r\nEspalexa library v2.4.0 by Christian Schwinne 2019"; server->send(200, "text/plain", res); } + #endif //not found URI (only if internal webserver is used) void serveNotFound() @@ -194,7 +254,9 @@ class Espalexa { EA_DEBUG("Received body: "); EA_DEBUGLN(body); }); + #ifndef ESPALEXA_NO_SUBPAGE serverAsync->on("/espalexa", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; servePage();}); + #endif serverAsync->on("/description.xml", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; serveDescription();}); serverAsync->begin(); @@ -208,57 +270,14 @@ class Espalexa { server->onNotFound([=](){serveNotFound();}); } + #ifndef ESPALEXA_NO_SUBPAGE server->on("/espalexa", HTTP_GET, [=](){servePage();}); + #endif 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() { @@ -286,11 +305,6 @@ class Espalexa { espalexaUdp.endPacket(); } - String boolString(bool st) - { - return(st)?"true":"false"; - } - public: Espalexa(){} @@ -348,10 +362,11 @@ class Espalexa { packetBuffer[len] = 0; } espalexaUdp.flush(); - + if (!discoverable) return; //do not reply to M-SEARCH if not discoverable + String request = packetBuffer; - EA_DEBUGLN(request); if(request.indexOf("M-SEARCH") >= 0) { + EA_DEBUGLN(request); if(request.indexOf("upnp:rootdevice") > 0 || request.indexOf("asic:1") > 0) { EA_DEBUGLN("Responding search req..."); respondToSearch(); @@ -364,12 +379,15 @@ class Espalexa { EA_DEBUG("Adding device "); EA_DEBUGLN((currentDeviceCount+1)); if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return false; + if (d == nullptr) return false; + d->setId(currentDeviceCount); devices[currentDeviceCount] = d; currentDeviceCount++; return true; } - - bool addDevice(String deviceName, CallbackBriFunction callback, uint8_t initialValue = 0) + + //deprecated brightness-only callback + bool addDevice(String deviceName, BrightnessCallbackFunction callback, uint8_t initialValue = 0) { EA_DEBUG("Constructing device "); EA_DEBUGLN((currentDeviceCount+1)); @@ -378,12 +396,12 @@ class Espalexa { return addDevice(d); } - bool addDevice(String deviceName, CallbackColFunction callback, uint8_t initialValue = 0) + bool addDevice(String deviceName, DeviceCallbackFunction callback, EspalexaDeviceType t = EspalexaDeviceType::dimmable, 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); + EspalexaDevice* d = new EspalexaDevice(deviceName, callback, t, initialValue); return addDevice(d); } @@ -413,32 +431,78 @@ class Espalexa { { EA_DEBUGLN("devType"); body = ""; - server->send(200, "application/json", "[{\"success\":{\"username\": \"2WLEDHardQrI3WHYTHoMcXHgEspsM8ZZRpSKtBQr\"}}]"); + 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); + + int devId = req.substring(req.indexOf("lights")+7).toInt(); + EA_DEBUG("ls"); EA_DEBUGLN(devId); + devices[devId-1]->setPropertyChanged(EspalexaDeviceProperty::none); + + if (body.indexOf("false")>0) //OFF command + { + devices[devId-1]->setValue(0); + devices[devId-1]->setPropertyChanged(EspalexaDeviceProperty::off); + devices[devId-1]->doCallback(); + return true; + } + + if (body.indexOf("true") >0) //ON command + { + devices[devId-1]->setValue(devices[devId-1]->getLastValue()); + devices[devId-1]->setPropertyChanged(EspalexaDeviceProperty::on); + } + + if (body.indexOf("bri") >0) //BRIGHTNESS command + { + uint8_t briL = body.substring(body.indexOf("bri") +5).toInt(); + if (briL == 255) + { + devices[devId-1]->setValue(255); + } else { + devices[devId-1]->setValue(briL+1); + } + devices[devId-1]->setPropertyChanged(EspalexaDeviceProperty::bri); + } + + if (body.indexOf("xy") >0) //COLOR command (XY mode) + { + devices[devId-1]->setColorXY(body.substring(body.indexOf("[") +1).toFloat(), body.substring(body.indexOf(",0") +1).toFloat()); + devices[devId-1]->setPropertyChanged(EspalexaDeviceProperty::xy); + } + + if (body.indexOf("hue") >0) //COLOR command (HS mode) + { + devices[devId-1]->setColor(body.substring(body.indexOf("hue") +5).toInt(), body.substring(body.indexOf("sat") +5).toInt()); + devices[devId-1]->setPropertyChanged(EspalexaDeviceProperty::hs); + } + + if (body.indexOf("ct") >0) //COLOR TEMP command (white spectrum) + { + devices[devId-1]->setColor(body.substring(body.indexOf("ct") +4).toInt()); + devices[devId-1]->setPropertyChanged(EspalexaDeviceProperty::ct); + } + + devices[devId-1]->doCallback(); + #ifdef ESPALEXA_DEBUG + if (devices[devId-1]->getLastChangedProperty() == EspalexaDeviceProperty::none) + EA_DEBUGLN("STATE REQ WITHOUT BODY (likely Content-Type issue #6)"); + #endif 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); + int devId = req.substring(pos+7).toInt(); + EA_DEBUG("l"); EA_DEBUGLN(devId); - if (tempDeviceId == 0) //client wants all lights + if (devId == 0) //client wants all lights { EA_DEBUGLN("lAll"); String jsonTemp = "{"; @@ -450,9 +514,9 @@ class Espalexa { } jsonTemp += "}"; server->send(200, "application/json", jsonTemp); - } else //client wants one light (tempDeviceId) + } else //client wants one light (devId) { - server->send(200, "application/json", deviceJsonString(tempDeviceId)); + server->send(200, "application/json", deviceJsonString(devId)); } return true; @@ -463,21 +527,26 @@ class Espalexa { return true; } - //is an unique device ID - String getEscapedMac() + //set whether Alexa can discover any devices + void setDiscoverable(bool d) { - return escapedMac; + discoverable = d; } - //convert brightness (0-255) to percentage - uint8_t toPercent(uint8_t bri) + //get EspalexaDevice at specific index + EspalexaDevice* getDevice(uint8_t index) { - uint16_t perc = bri * 100; - return perc / 255; + if (index >= currentDeviceCount) return nullptr; + return devices[index]; + } + + //is an unique device ID + String getEscapedMac() + { + return escapedMac; } ~Espalexa(){delete devices;} //note: Espalexa is NOT meant to be destructed }; #endif - diff --git a/src/EspalexaDevice.cpp b/src/EspalexaDevice.cpp index 70f5cdb..f440b80 100644 --- a/src/EspalexaDevice.cpp +++ b/src/EspalexaDevice.cpp @@ -4,34 +4,40 @@ EspalexaDevice::EspalexaDevice(){} -EspalexaDevice::EspalexaDevice(String deviceName, CallbackBriFunction gnCallback, uint8_t initialValue) { //constructor +EspalexaDevice::EspalexaDevice(String deviceName, BrightnessCallbackFunction gnCallback, uint8_t initialValue) { //constructor _deviceName = deviceName; _callback = gnCallback; _val = initialValue; _val_last = _val; + _type = EspalexaDeviceType::dimmable; } -EspalexaDevice::EspalexaDevice(String deviceName, CallbackColFunction gnCallback, uint8_t initialValue) { //constructor for color device +EspalexaDevice::EspalexaDevice(String deviceName, DeviceCallbackFunction gnCallback, EspalexaDeviceType t, uint8_t initialValue) { //constructor for color device _deviceName = deviceName; - _callbackCol = gnCallback; + _callbackDev = gnCallback; _callback = nullptr; + _type = t; _val = initialValue; _val_last = _val; } EspalexaDevice::~EspalexaDevice(){/*nothing to destruct*/} -bool EspalexaDevice::isColorDevice() +uint8_t EspalexaDevice::getId() { - //if brightness-only callback is null, we have color device - return (_callback == nullptr); + return _id; } -bool EspalexaDevice::isColorTemperatureMode() +EspalexaColorMode EspalexaDevice::getColorMode() { - return _ct; + return _mode; +} + +EspalexaDeviceType EspalexaDevice::getType() +{ + return _type; } String EspalexaDevice::getName() @@ -39,7 +45,7 @@ String EspalexaDevice::getName() return _deviceName; } -uint8_t EspalexaDevice::getLastChangedProperty() +EspalexaDeviceProperty EspalexaDevice::getLastChangedProperty() { return _changed; } @@ -49,6 +55,17 @@ uint8_t EspalexaDevice::getValue() return _val; } +uint8_t EspalexaDevice::getPercent() +{ + uint16_t perc = _val * 100; + return perc / 255; +} + +uint8_t EspalexaDevice::getDegrees() +{ + return getPercent(); +} + uint16_t EspalexaDevice::getHue() { return _hue; @@ -59,17 +76,37 @@ uint8_t EspalexaDevice::getSat() return _sat; } +float EspalexaDevice::getX() +{ + return _x; +} + +float EspalexaDevice::getY() +{ + return _y; +} + uint16_t EspalexaDevice::getCt() { if (_ct == 0) return 500; return _ct; } -uint32_t EspalexaDevice::getColorRGB() +uint32_t EspalexaDevice::getKelvin() { + if (_ct == 0) return 2000; + return 1000000/_ct; +} + +uint32_t EspalexaDevice::getRGB() +{ + if (_rgb != 0) return _rgb; //color has not changed uint8_t rgb[3]; + float r, g, b; - if (isColorTemperatureMode()) + if (_mode == EspalexaColorMode::none) return 0; + + if (_mode == EspalexaColorMode::ct) { //TODO tweak a bit to match hue lamp characteristics //based on https://gist.github.com/paulkaplan/5184275 @@ -97,9 +134,8 @@ uint32_t EspalexaDevice::getColorRGB() 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 + } else if (_mode == EspalexaColorMode::hs) + { float h = ((float)_hue)/65535.0; float s = ((float)_sat)/255.0; byte i = floor(h*6); @@ -115,8 +151,79 @@ uint32_t EspalexaDevice::getColorRGB() case 4: rgb[0]=t,rgb[1]=p,rgb[2]=255;break; case 5: rgb[0]=255,rgb[1]=p,rgb[2]=q; } + } else if (_mode == EspalexaColorMode::xy) + { + //Source: https://www.developers.meethue.com/documentation/color-conversions-rgb-xy + float z = 1.0f - _x - _y; + float X = (1.0f / _y) * _x; + float Z = (1.0f / _y) * z; + float r = (int)255*(X * 1.656492f - 0.354851f - Z * 0.255038f); + float g = (int)255*(-X * 0.707196f + 1.655397f + Z * 0.036152f); + float b = (int)255*(X * 0.051713f - 0.121364f + Z * 1.011530f); + if (r > b && r > g && r > 1.0f) { + // red is too big + g = g / r; + b = b / r; + r = 1.0f; + } else if (g > b && g > r && g > 1.0f) { + // green is too big + r = r / g; + b = b / g; + g = 1.0f; + } else if (b > r && b > g && b > 1.0f) { + // blue is too big + r = r / b; + g = g / b; + b = 1.0f; + } + // Apply gamma correction + r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * pow(r, (1.0f / 2.4f)) - 0.055f; + g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f; + b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f; + + if (r > b && r > g) { + // red is biggest + if (r > 1.0f) { + g = g / r; + b = b / r; + r = 1.0f; + } + } else if (g > b && g > r) { + // green is biggest + if (g > 1.0f) { + r = r / g; + b = b / g; + g = 1.0f; + } + } else if (b > r && b > g) { + // blue is biggest + if (b > 1.0f) { + r = r / b; + g = g / b; + b = 1.0f; + } + } + rgb[0] = 255.0*r; + rgb[1] = 255.0*g; + rgb[2] = 255.0*b; } - return ((rgb[0] << 16) | (rgb[1] << 8) | (rgb[2])); + _rgb = ((rgb[0] << 16) | (rgb[1] << 8) | (rgb[2])); + return _rgb; +} + +uint8_t EspalexaDevice::getR() +{ + return (getRGB() >> 16) & 0xFF; +} + +uint8_t EspalexaDevice::getG() +{ + return (getRGB() >> 8) & 0xFF; +} + +uint8_t EspalexaDevice::getB() +{ + return getRGB() & 0xFF; } uint8_t EspalexaDevice::getLastValue() @@ -125,12 +232,16 @@ uint8_t EspalexaDevice::getLastValue() return _val_last; } -void EspalexaDevice::setPropertyChanged(uint8_t p) +void EspalexaDevice::setPropertyChanged(EspalexaDeviceProperty p) { - //0: initial 1: on 2: off 3: bri 4: col 5: ct _changed = p; } +void EspalexaDevice::setId(uint8_t id) +{ + _id = id; +} + //you need to re-discover the device for the Alexa name to change void EspalexaDevice::setName(String name) { @@ -158,19 +269,41 @@ void EspalexaDevice::setPercent(uint8_t perc) setValue(val); } +void EspalexaDevice::setColorXY(float x, float y) +{ + _x = x; + _y = y; + _rgb = 0; + _mode = EspalexaColorMode::xy; +} + void EspalexaDevice::setColor(uint16_t hue, uint8_t sat) { _hue = hue; _sat = sat; - _ct = 0; + _rgb = 0; + _mode = EspalexaColorMode::hs; } void EspalexaDevice::setColor(uint16_t ct) { _ct = ct; + _rgb = 0; + _mode =EspalexaColorMode::ct; +} + +void EspalexaDevice::setColor(uint8_t r, uint8_t g, uint8_t b) +{ + float X = r * 0.664511f + g * 0.154324f + b * 0.162028f; + float Y = r * 0.283881f + g * 0.668433f + b * 0.047685f; + float Z = r * 0.000088f + g * 0.072310f + b * 0.986039f; + _x = X / (X + Y + Z); + _y = Y / (X + Y + Z); + _rgb = ((r << 16) | (g << 8) | b); + _mode = EspalexaColorMode::xy; } void EspalexaDevice::doCallback() { - (_callback != nullptr) ? _callback(_val) : _callbackCol(_val, getColorRGB()); + (_callback != nullptr) ? _callback(_val) : _callbackDev(this); } \ No newline at end of file diff --git a/src/EspalexaDevice.h b/src/EspalexaDevice.h index 0974ff3..07708d0 100644 --- a/src/EspalexaDevice.h +++ b/src/EspalexaDevice.h @@ -3,40 +3,63 @@ #include "Arduino.h" -typedef void (*CallbackBriFunction) (uint8_t br); -typedef void (*CallbackColFunction) (uint8_t br, uint32_t col); +typedef class EspalexaDevice; + +typedef void (*BrightnessCallbackFunction) (uint8_t b); +typedef void (*DeviceCallbackFunction) (EspalexaDevice* d); + +enum class EspalexaColorMode : uint8_t { none = 0, ct = 1, hs = 2, xy = 3 }; +enum class EspalexaDeviceType : uint8_t { onoff = 0, dimmable = 1, whitespectrum = 2, color = 3, extendedcolor = 4 }; +enum class EspalexaDeviceProperty : uint8_t { none = 0, on = 1, off = 2, bri = 3, hs = 4, ct = 5, xy = 6 }; class EspalexaDevice { private: String _deviceName; - CallbackBriFunction _callback; - CallbackColFunction _callbackCol; + BrightnessCallbackFunction _callback; + DeviceCallbackFunction _callbackDev; uint8_t _val, _val_last, _sat = 0; uint16_t _hue = 0, _ct = 0; - uint8_t _changed = 0; + float _x = 0, _y = 0; + uint32_t _rgb = 0; + uint8_t _id = 0; + EspalexaDeviceType _type; + EspalexaDeviceProperty _changed = EspalexaDeviceProperty::none; + EspalexaColorMode _mode; public: EspalexaDevice(); ~EspalexaDevice(); - EspalexaDevice(String deviceName, CallbackBriFunction gnCallback, uint8_t initialValue =0); - EspalexaDevice(String deviceName, CallbackColFunction gnCallback, uint8_t initialValue =0); + EspalexaDevice(String deviceName, BrightnessCallbackFunction bcb, uint8_t initialValue =0); + EspalexaDevice(String deviceName, DeviceCallbackFunction dcb, EspalexaDeviceType t =EspalexaDeviceType::dimmable, uint8_t initialValue =0); - bool isColorDevice(); - bool isColorTemperatureMode(); String getName(); - uint8_t getLastChangedProperty(); + uint8_t getId(); + EspalexaDeviceProperty getLastChangedProperty(); uint8_t getValue(); + uint8_t getPercent(); + uint8_t getDegrees(); uint16_t getHue(); uint8_t getSat(); uint16_t getCt(); - uint32_t getColorRGB(); + uint32_t getKelvin(); + float getX(); + float getY(); + uint32_t getRGB(); + uint8_t getR(); + uint8_t getG(); + uint8_t getB(); + EspalexaColorMode getColorMode(); + EspalexaDeviceType getType(); - void setPropertyChanged(uint8_t p); + void setId(uint8_t id); + void setPropertyChanged(EspalexaDeviceProperty 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 setColor(uint16_t hue, uint8_t sat); + void setColorXY(float x, float y); + void setColor(uint8_t r, uint8_t g, uint8_t b); void doCallback();