diff --git a/README.md b/README.md index 7fdfadb54..8edc4f8e2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Sonoff-Tasmota Provide ESP8266 based Sonoff by [iTead Studio](https://www.itead.cc/) and ElectroDragon IoT Relay with Serial, Web and MQTT control allowing 'Over the Air' or OTA firmware updates using Arduino IDE. -Current version is **3.9.19** - See ```sonoff/_releasenotes.ino``` for change information. +Current version is **3.9.20** - See ```sonoff/_releasenotes.ino``` for change information. - This version provides all (Sonoff) modules in one file and starts up with Sonoff Basic. - Once uploaded select module using the configuration webpage or the commands ```Modules``` and ```Module```. diff --git a/api/arduino/sonoff.ino.bin b/api/arduino/sonoff.ino.bin index 391f562b6..5064ad5c4 100644 Binary files a/api/arduino/sonoff.ino.bin and b/api/arduino/sonoff.ino.bin differ diff --git a/sonoff/_releasenotes.ino b/sonoff/_releasenotes.ino index 23df798ff..9fbfddea4 100644 --- a/sonoff/_releasenotes.ino +++ b/sonoff/_releasenotes.ino @@ -1,4 +1,8 @@ -/* 3.9.19 20170219 +/* 3.9.20 20170221 + * Add minimal basic authentication to Web Admin mode (#87) + * Fix Hue and add HSB support (#89) + * + * 3.9.19 20170219 * Sonoff Led: Made GPIO04, 05 and 15 available for user * Sonoff Led: Add commands Fade, Speed, WakupDuration, Wakeup and LedTable * diff --git a/sonoff/settings.h b/sonoff/settings.h index 049cb3410..6da85fe97 100644 --- a/sonoff/settings.h +++ b/sonoff/settings.h @@ -184,5 +184,6 @@ struct SYSCFG { uint8_t emulation; + char web_password[33]; } sysCfg; diff --git a/sonoff/settings.ino b/sonoff/settings.ino index 61f8f4e65..1abd9b1d9 100644 --- a/sonoff/settings.ino +++ b/sonoff/settings.ino @@ -437,7 +437,8 @@ void CFG_DefaultSet2() sysCfg.mqtt_enabled = MQTT_USE; sysCfg.emulation = EMULATION; - + + strlcpy(sysCfg.web_password, WEB_PASSWORD, sizeof(sysCfg.web_password)); } void CFG_Default() @@ -625,6 +626,9 @@ void CFG_Delta() if (sysCfg.version < 0x03090700) { // 3.9.7 - Add parameter sysCfg.emulation = EMULATION; } + if (sysCfg.version < 0x03091301) { + strlcpy(sysCfg.web_password, WEB_PASSWORD, sizeof(sysCfg.web_password)); + } sysCfg.version = VERSION; } diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index 318f3f633..d68449037 100644 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -10,7 +10,7 @@ * ==================================================== */ -#define VERSION 0x03091300 // 3.9.19 +#define VERSION 0x03091400 // 3.9.20 //#define BE_MINIMAL // Compile a minimal version if upgrade memory gets tight (still 404k) // To be used as step 1. Next step is compile and use desired version @@ -909,6 +909,16 @@ void mqttDataCb(char* topic, byte* data, unsigned int data_len) snprintf_P(svalue, sizeof(svalue), PSTR("{\"Webserver\":\"%s\"}"), MQTT_STATUS_OFF); } } + else if (!strcmp(type,"WEBPASSWORD")) { + if ((data_len > 0) && (data_len < sizeof(sysCfg.web_password))) { + if (payload == 0) { + sysCfg.web_password[0] = 0; // No password + } else { + strlcpy(sysCfg.web_password, (payload == 1) ? WEB_PASSWORD : dataBuf, sizeof(sysCfg.web_password)); + } + } + snprintf_P(svalue, sizeof(svalue), PSTR("{\"WebPassword\":\"%s\"}"), sysCfg.web_password); + } else if (!strcmp(type,"WEBLOG")) { if ((data_len > 0) && (payload >= LOG_LEVEL_NONE) && (payload <= LOG_LEVEL_ALL)) { sysCfg.weblog_level = payload; diff --git a/sonoff/user_config.h b/sonoff/user_config.h index d2b90a036..0581ad6ed 100644 --- a/sonoff/user_config.h +++ b/sonoff/user_config.h @@ -78,8 +78,10 @@ // -- HTTP ---------------------------------------- #define USE_WEBSERVER // Enable web server and wifi manager (+62k code, +4k mem) - Disable by // - #define FRIENDLY_NAME "Sonoff" // [FriendlyName] Friendlyname up to 32 characters used by webpages and Alexa #define WEB_SERVER 2 // [WebServer] Web server (0 = Off, 1 = Start as User, 2 = Start as Admin) + #define WEB_USERNAME "admin" // Web server Admin mode user name + #define WEB_PASSWORD "" // [WebPassword] Web server Admin mode Password for WEB_USERNAME (empty string = Disable) + #define FRIENDLY_NAME "Sonoff" // [FriendlyName] Friendlyname up to 32 characters used by webpages and Alexa #define USE_EMULATION // Enable Belkin WeMo and Hue Bridge emulation for Alexa (+11k code, +2k mem) #define EMULATION EMUL_NONE // [Emulation] Select Belkin WeMo or Hue Bridge emulation (EMUL_NONE, EMUL_WEMO or EMUL_HUE) diff --git a/sonoff/webserver.ino b/sonoff/webserver.ino index 25080590c..ab4332408 100644 --- a/sonoff/webserver.ino +++ b/sonoff/webserver.ino @@ -171,6 +171,7 @@ const char HTTP_FORM_LOG3[] PROGMEM = const char HTTP_FORM_OTHER[] PROGMEM = "
 Other parameters 
" "" + "
Web Admin Password

" "
MQTT enable
"; const char HTTP_FORM_OTHER2[] PROGMEM = "
Friendly Name {1 ({2)

"; @@ -219,120 +220,6 @@ const char HTTP_END[] PROGMEM = "" "" ""; -#ifdef USE_EMULATION -const char WEMO_EVENTSERVICE_XML[] PROGMEM = - "" - "" - "" - "SetBinaryState" - "" - "" - "" - "BinaryState" - "BinaryState" - "in" - "" - "" - "" - "" - "BinaryState" - "Boolean" - "0" - "" - "" - "level" - "string" - "0" - "" - "" - "" - "\r\n" - "\r\n"; -const char WEMO_SETUP_XML[] PROGMEM = - "" - "" - "" - "urn:Belkin:device:controllee:1" - "{x1}" - "Belkin International Inc." - "Sonoff Socket" - "3.1415" - "uuid:{x2}" - "{x3}" - "0" - "" - "" - "urn:Belkin:service:basicevent:1" - "urn:Belkin:serviceId:basicevent1" - "/upnp/control/basicevent1" - "/upnp/event/basicevent1" - "/eventservice.xml" - "" - "" - "" - "\r\n" - "\r\n"; -const char HUE_DESCRIPTION_XML[] PROGMEM = - "" - "" - "" - "1" - "0" - "" - "http://{x1}/" - "" - "urn:schemas-upnp-org:device:Basic:1" - "Amazon-Echo-HA-Bridge ({x1})" - "Royal Philips Electronics" - "Philips hue bridge 2012" - "929000226503" - "uuid:{x2}" - "" - "\r\n" - "\r\n"; -const char HUE_LIGHT_STATUS_JSON[] PROGMEM = - "{\"state\":" - "{\"on\":{state}," - "\"bri\":{b}," - "\"hue\":{h}," - "\"sat\":{s}," - "\"effect\":\"none\"," - "\"ct\":0," - "\"alert\":\"none\"," - "\"reachable\":true" - "}," - "\"type\":\"Dimmable light\"," - "\"name\":\"{j1}\"," - "\"modelid\":\"LWB004\"," - "\"manufacturername\":\"Philips\"," - "\"uniqueid\":\"{j2}\"," - "\"swversion\":\"66012040\"" - "}"; -const char HUE_LIGHT_RESPONSE_JSON[] PROGMEM = - "{\"success\":{\"{api}/{id}/{cmd}\":{res}}}"; -const char HUE_CONFIG_RESPONSE_JSON[] PROGMEM = - "{\"name\":\"Philips hue\"," - "\"mac\":\"{mac}\"," - "\"dhcp\":true," - "\"ipaddress\":\"{ip}\"," - "\"netmask\":\"{mask}\"," - "\"gateway\":\"{gw}\"," - "\"proxyaddress\":\"\"," - "\"proxyport\":0," - "\"UTC\":\"{dt}\"," - "\"whitelist\":{\"{id}\":{" - "\"last use date\":\"{dt}\"," - "\"create date\":\"{dt}\"," - "\"name\":\"Remote\"}}," - "\"swversion\":\"01036659\"," - "\"apiversion\":\"1.16.0\"," - "\"swupdate\":{\"updatestate\":0,\"url\":\"\",\"text\":\"\",\"notify\": false}," - "\"linkbutton\":false," - "\"portalservices\":false" - "}"; -const char HUE_NO_AUTH_JSON[] PROGMEM = - "[{\"error\":{\"type\":101,\"address\":\"/\",\"description\":\"link button not pressed\"}}]"; -#endif // USE_EMULATION #define DNS_PORT 53 enum http_t {HTTP_OFF, HTTP_USER, HTTP_ADMIN, HTTP_MANAGER}; @@ -383,7 +270,9 @@ void startWebserver(int type, IPAddress ipweb) webServer->on("/eventservice.xml", handleUPnPservice); webServer->on("/setup.xml", handleUPnPsetupWemo); } - if (sysCfg.emulation == EMUL_HUE) webServer->on("/description.xml", handleUPnPsetupHue); + if (sysCfg.emulation == EMUL_HUE) { + webServer->on("/description.xml", handleUPnPsetupHue); + } #endif // USE_EMULATION webServer->onNotFound(handleNotFound); } @@ -437,6 +326,9 @@ void pollDnsWeb() void showPage(String &page) { + if((_httpflag == HTTP_ADMIN) && (sysCfg.web_password[0] != 0) && !webServer->authenticate(WEB_USERNAME, sysCfg.web_password)) { + return webServer->requestAuthentication(); + } page.replace("{ha}", my_module.name); page.replace("{h}", sysCfg.friendlyname[0]); if (_httpflag == HTTP_MANAGER) { @@ -783,6 +675,7 @@ void handleOther() String page = FPSTR(HTTP_HEAD); page.replace("{v}", "Configure Other"); page += FPSTR(HTTP_FORM_OTHER); + page.replace("{p1}", sysCfg.web_password); page.replace("{r1}", (sysCfg.mqtt_enabled) ? " checked" : ""); page += FPSTR(HTTP_FORM_OTHER2); page.replace("{1", "1"); @@ -884,6 +777,8 @@ void handleSave() break; #endif // USE_DOMOTICZ case 5: + strlcpy(sysCfg.web_password, (!strlen(webServer->arg("p1").c_str())) ? WEB_PASSWORD : webServer->arg("p1").c_str(), sizeof(sysCfg.web_password)); + if (sysCfg.web_password[0] == '0') sysCfg.web_password[0] = '\0'; sysCfg.mqtt_enabled = webServer->hasArg("b1"); #ifdef USE_EMULATION sysCfg.emulation = (!strlen(webServer->arg("b2").c_str())) ? 0 : atoi(webServer->arg("b2").c_str()); @@ -1176,32 +1071,43 @@ void handleCmnd() addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle cmnd")); - byte curridx = logidx; - if (strlen(webServer->arg(0).c_str())) { - snprintf_P(svalue, sizeof(svalue), webServer->arg(0).c_str()); - do_cmnd(svalue); + uint8_t valid = 1; + if (sysCfg.web_password[0] != 0) { + if (!(!strcmp(webServer->arg("user").c_str(),WEB_USERNAME) && !strcmp(webServer->arg("password").c_str(),sysCfg.web_password))) { + valid = 0; + } } String message = ""; - if (logidx != curridx) { - byte counter = curridx; - do { - if (Log[counter].length()) { - if (message.length()) message += F("\n"); - if (sysCfg.mqtt_enabled) { - // [14:49:36 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"}] > [RESULT = {"POWER":"OFF"}] -// message += Log[counter].substring(17 + strlen(PUB_PREFIX) + strlen(sysCfg.mqtt_topic)); - message += Log[counter].substring(Log[counter].lastIndexOf("/",Log[counter].indexOf("="))+1); - } else { - // [14:49:36 RSLT: RESULT = {"POWER":"OFF"}] > [RESULT = {"POWER":"OFF"}] - message += Log[counter].substring(Log[counter].indexOf(": ")+2); + if (valid) { + byte curridx = logidx; + if (strlen(webServer->arg("cmnd").c_str())) { + snprintf_P(svalue, sizeof(svalue), webServer->arg("cmnd").c_str()); + do_cmnd(svalue); + } + + if (logidx != curridx) { + byte counter = curridx; + do { + if (Log[counter].length()) { + if (message.length()) message += F("\n"); + if (sysCfg.mqtt_enabled) { + // [14:49:36 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"}] > [RESULT = {"POWER":"OFF"}] +// message += Log[counter].substring(17 + strlen(PUB_PREFIX) + strlen(sysCfg.mqtt_topic)); + message += Log[counter].substring(Log[counter].lastIndexOf("/",Log[counter].indexOf("="))+1); + } else { + // [14:49:36 RSLT: RESULT = {"POWER":"OFF"}] > [RESULT = {"POWER":"OFF"}] + message += Log[counter].substring(Log[counter].indexOf(": ")+2); + } } - } - counter++; - if (counter > MAX_LOG_LINES -1) counter = 0; - } while (counter != logidx); + counter++; + if (counter > MAX_LOG_LINES -1) counter = 0; + } while (counter != logidx); + } else { + message = F("Enable weblog 2 if response expected\n"); + } } else { - message = F("Enable weblog 2 if response expected\n"); + message = F("Need user=&password=\n"); } webServer->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); @@ -1313,17 +1219,11 @@ void handleInfo() page += F("Emulation"); #ifdef USE_EMULATION - if (sysCfg.emulation == EMUL_WEMO) { - page += F("Belkin WeMo"); - } - else if (sysCfg.emulation == EMUL_HUE) { - page += F("Hue Bridge"); - } - else { - page += F("None"); - } + if (sysCfg.emulation == EMUL_WEMO) page += F("Belkin WeMo"); + else if (sysCfg.emulation == EMUL_HUE) page += F("Hue Bridge"); + else page += F("None"); #else - page += F("Disabled"); + page += F("Disabled"); #endif // USE_EMULATION page += F(""); @@ -1376,276 +1276,6 @@ void handleRestart() /********************************************************************************************/ -#ifdef USE_EMULATION -void handleUPnPevent() -{ - addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo basic event")); - - String request = webServer->arg(0); - if (request.indexOf("State>1 0) do_cmnd_power(1, 1); - if (request.indexOf("State>0 0) do_cmnd_power(1, 0); - webServer->send(200, "text/plain", ""); -} - -void handleUPnPservice() -{ - addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo event service")); - - String eventservice_xml = FPSTR(WEMO_EVENTSERVICE_XML); - webServer->send(200, "text/plain", eventservice_xml); -} - -void handleUPnPsetupWemo() -{ - addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo setup")); - - String setup_xml = FPSTR(WEMO_SETUP_XML); - setup_xml.replace("{x1}", sysCfg.friendlyname[0]); - setup_xml.replace("{x2}", wemo_UUID()); - setup_xml.replace("{x3}", wemo_serial()); - webServer->send(200, "text/xml", setup_xml); -} - -/********************************************************************************************/ - -String hue_deviceId(uint8_t id) -{ - char deviceid[16]; - - snprintf_P(deviceid, sizeof(deviceid), PSTR("5CCF7F%03X-%0d"), ESP.getChipId(), id); - return String(deviceid); -} - -void handleUPnPsetupHue() -{ - addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle Hue Bridge setup")); - - String description_xml = FPSTR(HUE_DESCRIPTION_XML); - description_xml.replace("{x1}", WiFi.localIP().toString()); - description_xml.replace("{x2}", hue_UUID()); - webServer->send(200, "text/xml", description_xml); -} - -void hue_todo(String *path) -{ - char log[LOGSZ]; - - snprintf_P(log, sizeof(log), PSTR("HTTP: HUE API not implemented (%s)"),path->c_str()); - addLog(LOG_LEVEL_DEBUG_MORE, log); -} - -void hue_config_response(String *response) -{ - char buffer[21]; - - *response += FPSTR(HUE_CONFIG_RESPONSE_JSON); - response->replace("{mac}", WiFi.macAddress()); - response->replace("{ip}", WiFi.localIP().toString()); - response->replace("{mask}", WiFi.subnetMask().toString()); - response->replace("{gw}", WiFi.gatewayIP().toString()); - snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02dT%02d:%02d:%02d"), - rtcTime.Year, rtcTime.Month, rtcTime.Day, rtcTime.Hour, rtcTime.Minute, rtcTime.Second); - response->replace("{dt}", buffer); -} - -void hue_global_cfg(String *path) -{ - String response; - - path->remove(0,1); // cut leading / to get - response = "{\"lights\":{\""; - for (uint8_t i = 1; i <= Maxdevice; i++) - { - response += i; - response += "\":"; - response += FPSTR(HUE_LIGHT_STATUS_JSON); - if (i < Maxdevice) response += ",\""; - response.replace("{state}", (power & (0x01 << (i-1))) ? "true" : "false"); - response.replace("{j1}", sysCfg.friendlyname[i-1]); - response.replace("{j2}", hue_deviceId(i)); - if (pin[GPIO_WS2812] < 99) { -#ifdef USE_WS2812 - ws2812_replaceHSB(&response); -#endif // USE_WS2812 - } else - { - response.replace("{h}", "0"); - response.replace("{s}", "0"); - response.replace("{b}", "0"); - } - } - response += F("},\"groups\":{},\"schedules\":{},\"config\":"); - - hue_config_response(&response); - response.replace("{id}", *path); - response += "}"; - webServer->send(200, "application/json", response); -} - -void hue_auth(String *path) -{ - char response[38]; - - snprintf_P(response, sizeof(response), PSTR("[{\"success\":{\"username\":\"%03x\"}}]"), ESP.getChipId()); - webServer->send(200, "application/json", response); -} - -void hue_config(String *path) -{ - String response = ""; - - path->remove(0,1); // cut leading / to get - hue_config_response(&response); - response.replace("{id}", *path); - webServer->send(200, "application/json", response); -} - -void hue_lights(String *path) -{ -/* - * http://sonoff/api/username/lights/1/state?1={"on":true,"hue":56100,"sat":254,"bri":254,"alert":"none","transitiontime":40} - */ - String response; - uint8_t device = 1; - int16_t pos = 0; - uint8_t bri = 0; - bool on = false; - char id[4]; - - path->remove(0,path->indexOf("/lights")); // Remove until /lights - if (path->endsWith("/lights")) // Got /lights - { - response = "{\""; - for (uint8_t i = 1; i <= Maxdevice; i++) - { - response += i; - response += "\":"; - response += FPSTR(HUE_LIGHT_STATUS_JSON); - if (i < Maxdevice) response += ",\""; - response.replace("{state}", (power & (0x01 << (i-1))) ? "true" : "false"); - response.replace("{j1}", sysCfg.friendlyname[i-1]); - response.replace("{j2}", hue_deviceId(i)); - if (pin[GPIO_WS2812] < 99) { -#ifdef USE_WS2812 - ws2812_replaceHSB(&response); -#endif // USE_WS2812 - } else - { - response.replace("{h}", "0"); - response.replace("{s}", "0"); - response.replace("{b}", "0"); - } - } - response += "}"; - webServer->send(200, "application/json", response); - } - else if (path->endsWith("/state")) // Got ID/state - { - path->remove(0,8); // Remove /lights/ - path->remove(path->indexOf("/state")); // Remove /state - device = atoi(path->c_str()); - if ((device < 1) || (device > Maxdevice)) device = 1; - response = "["; - response += FPSTR(HUE_LIGHT_RESPONSE_JSON); - response.replace("{api}", "/lights"); - response.replace("{id}", String(device)); - response.replace("{cmd}", "state/on"); - if (webServer->args() == 1) - { - StaticJsonBuffer<400> jsonBuffer; - JsonObject &hue_json = jsonBuffer.parseObject(webServer->arg(0)); - on = hue_json["on"]; - switch(on) - { - case false : do_cmnd_power(device, 0); - response.replace("{res}", "false"); - break; - case true : do_cmnd_power(device, 1); - response.replace("{res}", "true"); - break; - default : response.replace("{res}", (power & (0x01 << (device-1))) ? "true" : "false"); - break; - } -#ifdef USE_WS2812 - bri = hue_json["bri"]; - if (pin[GPIO_WS2812] < 99) { - ws2812_changeBrightness(bri); - response += ","; - response += FPSTR(HUE_LIGHT_RESPONSE_JSON); - response.replace("{api}", "/lights"); - response.replace("{id}", String(device)); - response.replace("{cmd}", "state/bri"); - response.replace("{res}", String(bri)); - } -#endif // USE_WS2812 - response += "]"; - webServer->send(200, "application/json", response); - } - else webServer->send(406, "application/json", "{}"); - } - else if(path->indexOf("/lights/") >= 0) { // Got /lights/ID - path->remove(0,8); // Remove /lights/ - device = atoi(path->c_str()); - if ((device < 1) || (device > Maxdevice)) device = 1; - response = FPSTR(HUE_LIGHT_STATUS_JSON); - response.replace("{state}", (power & (0x01 << (device -1))) ? "true" : "false"); - response.replace("{j1}", sysCfg.friendlyname[device -1]); - response.replace("{j2}", hue_deviceId(device)); - if (pin[GPIO_WS2812] < 99) { -#ifdef USE_WS2812 - ws2812_replaceHSB(&response); -#endif // USE_WS2812 - } else - { - response.replace("{h}", "0"); - response.replace("{s}", "0"); - response.replace("{b}", "0"); - } - webServer->send(200, "application/json", response); - } - else webServer->send(406, "application/json", "{}"); -} - -void handle_hue_api(String *path) -{ - /* HUE API uses /api// syntax. The userid is created by the echo device and - * on original HUE the pressed button allows for creation of this user. We simply ignore the - * user part and allow every caller as with Web or WeMo. - * - * (c) Heiko Krupp, 2017 - */ - - char log[LOGSZ]; - uint8_t args = 0; - - path->remove(0, 4); // remove /api - snprintf_P(log, sizeof(log), PSTR("HTTP: Handle Hue API (%s)"), path->c_str()); - addLog(LOG_LEVEL_DEBUG_MORE, log); - for (args = 0; args < webServer->args(); args++) { - String json = webServer->arg(args); - snprintf_P(log, sizeof(log), PSTR("HTTP: Hue POST args (%s)"), json.c_str()); - addLog(LOG_LEVEL_DEBUG_MORE, log); - } - - if (path->endsWith("/invalid/")) {} // Just ignore - else if (path->endsWith("/")) hue_auth(path); // New HUE App setup - else if (path->endsWith("/config")) hue_config(path); - else if (path->indexOf("/lights") >= 0) hue_lights(path); - else if (path->endsWith("/groups")) hue_todo(path); - else if (path->endsWith("/schedules")) hue_todo(path); - else if (path->endsWith("/sensors")) hue_todo(path); - else if (path->endsWith("/scenes")) hue_todo(path); - else if (path->endsWith("/rules")) hue_todo(path); - else hue_global_cfg(path); -/* - { - snprintf_P(log, sizeof(log), PSTR("HTTP: Handle Hue API (%s)"),path->c_str()); - addLog(LOG_LEVEL_DEBUG_MORE, log); - webServer->send(406, "application/json", "{}"); - } -*/ -} -#endif // USE_EMULATION /********************************************************************************************/ diff --git a/sonoff/xdrv_wemohue.ino b/sonoff/xdrv_wemohue.ino index 2a4472133..0c3a03731 100644 --- a/sonoff/xdrv_wemohue.ino +++ b/sonoff/xdrv_wemohue.ino @@ -216,5 +216,417 @@ void pollUDP() } } } + +#ifdef USE_WEBSERVER +/*********************************************************************************************\ + * Web server additions +\*********************************************************************************************/ +const char WEMO_EVENTSERVICE_XML[] PROGMEM = + "" + "" + "" + "SetBinaryState" + "" + "" + "" + "BinaryState" + "BinaryState" + "in" + "" + "" + "" + "" + "BinaryState" + "Boolean" + "0" + "" + "" + "level" + "string" + "0" + "" + "" + "" + "\r\n" + "\r\n"; +const char WEMO_SETUP_XML[] PROGMEM = + "" + "" + "" + "urn:Belkin:device:controllee:1" + "{x1}" + "Belkin International Inc." + "Sonoff Socket" + "3.1415" + "uuid:{x2}" + "{x3}" + "0" + "" + "" + "urn:Belkin:service:basicevent:1" + "urn:Belkin:serviceId:basicevent1" + "/upnp/control/basicevent1" + "/upnp/event/basicevent1" + "/eventservice.xml" + "" + "" + "" + "\r\n" + "\r\n"; +const char HUE_DESCRIPTION_XML[] PROGMEM = + "" + "" + "" + "1" + "0" + "" + "http://{x1}/" + "" + "urn:schemas-upnp-org:device:Basic:1" + "Amazon-Echo-HA-Bridge ({x1})" + "Royal Philips Electronics" + "Philips hue bridge 2012" + "929000226503" + "uuid:{x2}" + "" + "\r\n" + "\r\n"; +const char HUE_LIGHT_STATUS_JSON[] PROGMEM = + "{\"state\":" + "{\"on\":{state}," + "\"bri\":{b}," + "\"hue\":{h}," + "\"sat\":{s}," + "\"effect\":\"none\"," + "\"ct\":0," + "\"alert\":\"none\"," + "\"colormode\":\"hs\"," + "\"reachable\":true" + "}," + "\"type\":\"Dimmable light\"," + "\"name\":\"{j1}\"," + "\"modelid\":\"LWB004\"," + "\"manufacturername\":\"Philips\"," + "\"uniqueid\":\"{j2}\"," + "\"swversion\":\"66012040\"" + "}"; +const char HUE_LIGHT_RESPONSE_JSON[] PROGMEM = + "{\"success\":{\"{api}/{id}/{cmd}\":{res}}}"; +const char HUE_CONFIG_RESPONSE_JSON[] PROGMEM = + "{\"name\":\"Philips hue\"," + "\"mac\":\"{mac}\"," + "\"dhcp\":true," + "\"ipaddress\":\"{ip}\"," + "\"netmask\":\"{mask}\"," + "\"gateway\":\"{gw}\"," + "\"proxyaddress\":\"\"," + "\"proxyport\":0," + "\"UTC\":\"{dt}\"," + "\"whitelist\":{\"{id}\":{" + "\"last use date\":\"{dt}\"," + "\"create date\":\"{dt}\"," + "\"name\":\"Remote\"}}," + "\"swversion\":\"01036659\"," + "\"apiversion\":\"1.16.0\"," + "\"swupdate\":{\"updatestate\":0,\"url\":\"\",\"text\":\"\",\"notify\": false}," + "\"linkbutton\":false," + "\"portalservices\":false" + "}"; +const char HUE_ERROR_JSON[] PROGMEM = + "[{\"error\":{\"type\":901,\"address\":\"/\",\"description\":\"Internal Error\"}}]"; + +void handleUPnPevent() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo basic event")); + + String request = webServer->arg(0); + if (request.indexOf("State>1 0) do_cmnd_power(1, 1); + if (request.indexOf("State>0 0) do_cmnd_power(1, 0); + webServer->send(200, "text/plain", ""); +} + +void handleUPnPservice() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo event service")); + + String eventservice_xml = FPSTR(WEMO_EVENTSERVICE_XML); + webServer->send(200, "text/plain", eventservice_xml); +} + +void handleUPnPsetupWemo() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle WeMo setup")); + + String setup_xml = FPSTR(WEMO_SETUP_XML); + setup_xml.replace("{x1}", sysCfg.friendlyname[0]); + setup_xml.replace("{x2}", wemo_UUID()); + setup_xml.replace("{x3}", wemo_serial()); + webServer->send(200, "text/xml", setup_xml); +} + +/********************************************************************************************/ + +String hue_deviceId(uint8_t id) +{ + char deviceid[16]; + + snprintf_P(deviceid, sizeof(deviceid), PSTR("5CCF7F%03X-%0d"), ESP.getChipId(), id); + return String(deviceid); +} + +void handleUPnPsetupHue() +{ + addLog_P(LOG_LEVEL_DEBUG, PSTR("HTTP: Handle Hue Bridge setup")); + + String description_xml = FPSTR(HUE_DESCRIPTION_XML); + description_xml.replace("{x1}", WiFi.localIP().toString()); + description_xml.replace("{x2}", hue_UUID()); + webServer->send(200, "text/xml", description_xml); +} + +void hue_todo(String *path) +{ + char log[LOGSZ]; + + snprintf_P(log, sizeof(log), PSTR("HTTP: HUE API not implemented (%s)"),path->c_str()); + addLog(LOG_LEVEL_DEBUG_MORE, log); +} + +void hue_config_response(String *response) +{ + char buffer[21]; + + *response += FPSTR(HUE_CONFIG_RESPONSE_JSON); + response->replace("{mac}", WiFi.macAddress()); + response->replace("{ip}", WiFi.localIP().toString()); + response->replace("{mask}", WiFi.subnetMask().toString()); + response->replace("{gw}", WiFi.gatewayIP().toString()); + snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02dT%02d:%02d:%02d"), + rtcTime.Year, rtcTime.Month, rtcTime.Day, rtcTime.Hour, rtcTime.Minute, rtcTime.Second); + response->replace("{dt}", buffer); +} + +void hue_global_cfg(String *path) +{ + String response; + + path->remove(0,1); // cut leading / to get + response = "{\"lights\":{\""; + for (uint8_t i = 1; i <= Maxdevice; i++) + { + response += i; + response += "\":"; + response += FPSTR(HUE_LIGHT_STATUS_JSON); + if (i < Maxdevice) response += ",\""; + response.replace("{state}", (power & (0x01 << (i-1))) ? "true" : "false"); + response.replace("{j1}", sysCfg.friendlyname[i-1]); + response.replace("{j2}", hue_deviceId(i)); + if (pin[GPIO_WS2812] < 99) { +#ifdef USE_WS2812 + ws2812_replaceHSB(&response); +#endif // USE_WS2812 + } else + { + response.replace("{h}", "0"); + response.replace("{s}", "0"); + response.replace("{b}", "0"); + } + } + response += F("},\"groups\":{},\"schedules\":{},\"config\":"); + + hue_config_response(&response); + response.replace("{id}", *path); + response += "}"; + webServer->send(200, "application/json", response); +} + +void hue_auth(String *path) +{ + char response[38]; + + snprintf_P(response, sizeof(response), PSTR("[{\"success\":{\"username\":\"%03x\"}}]"), ESP.getChipId()); + webServer->send(200, "application/json", response); +} + +void hue_config(String *path) +{ + String response = ""; + + path->remove(0,1); // cut leading / to get + hue_config_response(&response); + response.replace("{id}", *path); + webServer->send(200, "application/json", response); +} + +void hue_lights(String *path) +{ +/* + * http://sonoff/api/username/lights/1/state?1={"on":true,"hue":56100,"sat":254,"bri":254,"alert":"none","transitiontime":40} + */ + String response; + uint8_t device = 1; + uint16_t tmp=0; + int16_t pos = 0; + float bri = 0; + float hue = 0; + float sat = 0; + bool on = false; + bool change = false; + char id[4]; + + path->remove(0,path->indexOf("/lights")); // Remove until /lights + if (path->endsWith("/lights")) { // Got /lights + response = "{\""; + for (uint8_t i = 1; i <= Maxdevice; i++) { + response += i; + response += "\":"; + response += FPSTR(HUE_LIGHT_STATUS_JSON); + if (i < Maxdevice) response += ",\""; + response.replace("{state}", (power & (0x01 << (i-1))) ? "true" : "false"); + response.replace("{j1}", sysCfg.friendlyname[i-1]); + response.replace("{j2}", hue_deviceId(i)); + if (pin[GPIO_WS2812] < 99) { +#ifdef USE_WS2812 + ws2812_replaceHSB(&response); +#endif // USE_WS2812 + } else { + response.replace("{h}", "0"); + response.replace("{s}", "0"); + response.replace("{b}", "0"); + } + } + response += "}"; + webServer->send(200, "application/json", response); + } + else if (path->endsWith("/state")) { // Got ID/state + path->remove(0,8); // Remove /lights/ + path->remove(path->indexOf("/state")); // Remove /state + device = atoi(path->c_str()); + if ((device < 1) || (device > Maxdevice)) device = 1; + response = "["; + response += FPSTR(HUE_LIGHT_RESPONSE_JSON); + response.replace("{api}", "/lights"); + response.replace("{id}", String(device)); + response.replace("{cmd}", "state/on"); + if (webServer->args() == 1) { + StaticJsonBuffer<400> jsonBuffer; + JsonObject &hue_json = jsonBuffer.parseObject(webServer->arg(0)); + if (hue_json.containsKey("on")) { + on = hue_json["on"]; + switch(on) + { + case false : do_cmnd_power(device, 0); + response.replace("{res}", "false"); + break; + case true : do_cmnd_power(device, 1); + response.replace("{res}", "true"); + break; + default : response.replace("{res}", (power & (0x01 << (device-1))) ? "true" : "false"); + break; + } + } +#ifdef USE_WS2812 + ws2812_getHSB(&hue,&sat,&bri); + if (hue_json.containsKey("bri")) { + tmp = hue_json["bri"]; + bri = (float)tmp/254.0f; + response += ","; + response += FPSTR(HUE_LIGHT_RESPONSE_JSON); + response.replace("{api}", "/lights"); + response.replace("{id}", String(device)); + response.replace("{cmd}", "state/bri"); + response.replace("{res}", String(tmp)); + change = true; + } + if (hue_json.containsKey("hue")) { + tmp = hue_json["hue"]; + hue = (float)tmp/65535.0f; + response += ","; + response += FPSTR(HUE_LIGHT_RESPONSE_JSON); + response.replace("{api}", "/lights"); + response.replace("{id}", String(device)); + response.replace("{cmd}", "state/hue"); + response.replace("{res}", String(tmp)); + change = true; + } + if (hue_json.containsKey("sat")) { + tmp = hue_json["sat"]; + sat = (float)tmp/254.0f; + response += ","; + response += FPSTR(HUE_LIGHT_RESPONSE_JSON); + response.replace("{api}", "/lights"); + response.replace("{id}", String(device)); + response.replace("{cmd}", "state/sat"); + response.replace("{res}", String(tmp)); + change = true; + } + if (change && (pin[GPIO_WS2812] < 99)) { + ws2812_setHSB(hue,sat,bri); + change=false; + } +#endif // USE_WS2812 + response += "]"; + webServer->send(200, "application/json", response); + } + else { + response=FPSTR(HUE_ERROR_JSON); + webServer->send(200, "application/json", response); + } + } + else if(path->indexOf("/lights/") >= 0) { // Got /lights/ID + path->remove(0,8); // Remove /lights/ + device = atoi(path->c_str()); + if ((device < 1) || (device > Maxdevice)) device = 1; + response = FPSTR(HUE_LIGHT_STATUS_JSON); + response.replace("{state}", (power & (0x01 << (device -1))) ? "true" : "false"); + response.replace("{j1}", sysCfg.friendlyname[device -1]); + response.replace("{j2}", hue_deviceId(device)); + if (pin[GPIO_WS2812] < 99) { +#ifdef USE_WS2812 + ws2812_replaceHSB(&response); +#endif // USE_WS2812 + } else { + response.replace("{h}", "0"); + response.replace("{s}", "0"); + response.replace("{b}", "0"); + } + webServer->send(200, "application/json", response); + } + else webServer->send(406, "application/json", "{}"); +} + +void handle_hue_api(String *path) +{ + /* HUE API uses /api// syntax. The userid is created by the echo device and + * on original HUE the pressed button allows for creation of this user. We simply ignore the + * user part and allow every caller as with Web or WeMo. + * + * (c) Heiko Krupp, 2017 + */ + + char log[LOGSZ]; + uint8_t args = 0; + + path->remove(0, 4); // remove /api + snprintf_P(log, sizeof(log), PSTR("HTTP: Handle Hue API (%s)"), path->c_str()); + addLog(LOG_LEVEL_DEBUG_MORE, log); + for (args = 0; args < webServer->args(); args++) { + String json = webServer->arg(args); + snprintf_P(log, sizeof(log), PSTR("HTTP: Hue POST args (%s)"), json.c_str()); + addLog(LOG_LEVEL_DEBUG_MORE, log); + } + + if (path->endsWith("/invalid/")) {} // Just ignore + else if (path->endsWith("/")) hue_auth(path); // New HUE App setup + else if (path->endsWith("/config")) hue_config(path); + else if (path->indexOf("/lights") >= 0) hue_lights(path); + else if (path->endsWith("/groups")) hue_todo(path); + else if (path->endsWith("/schedules")) hue_todo(path); + else if (path->endsWith("/sensors")) hue_todo(path); + else if (path->endsWith("/scenes")) hue_todo(path); + else if (path->endsWith("/rules")) hue_todo(path); + else hue_global_cfg(path); +} +#endif // USE_WEBSERVER #endif // USE_EMULATION diff --git a/sonoff/xdrv_ws2812.ino b/sonoff/xdrv_ws2812.ino index 0f6efd765..e9d7b58f7 100644 --- a/sonoff/xdrv_ws2812.ino +++ b/sonoff/xdrv_ws2812.ino @@ -44,8 +44,6 @@ POSSIBILITY OF SUCH DAMAGE. #endif // USE_WS2812_CTYPE #endif // USE_WS2812_DMA -#define COLOR_SATURATION 254.0f - struct wsColor { uint8_t red, green, blue; }; @@ -168,20 +166,27 @@ void ws2812_replaceHSB(String *response) ws2812_setDim(sysCfg.ws_dimmer); HsbColor hsb=HsbColor(dcolor); response->replace("{h}", String((uint16_t)(65535.0f * hsb.H))); - response->replace("{s}", String((uint8_t)(COLOR_SATURATION * hsb.S))); - response->replace("{b}", String((uint8_t)(COLOR_SATURATION * hsb.B))); + response->replace("{s}", String((uint8_t)(254.0f * hsb.S))); + response->replace("{b}", String((uint8_t)(254.0f * hsb.B))); } -void ws2812_changeBrightness(uint8_t bri) +void ws2812_getHSB(float *hue, float *sat, float *bri) +{ + ws2812_setDim(sysCfg.ws_dimmer); + HsbColor hsb=HsbColor(dcolor); + *hue=hsb.H; + *sat=hsb.S; + *bri=hsb.B; +} + +void ws2812_setHSB(float hue, float sat, float bri) { char rgb[7]; - //sysCfg.ws_ledtable=1; // Switch on Gamma Correction for "natural" brightness controll - ws2812_setDim(sysCfg.ws_dimmer); - HsbColor hsb = HsbColor(dcolor); - if (!bri) bri=1; - if (bri==255) bri=252; - hsb.B=(float)(bri/COLOR_SATURATION); + HsbColor hsb; + hsb.H=hue; + hsb.S=sat; + hsb.B=bri; RgbColor tmp = RgbColor(hsb); sprintf(rgb,"%02X%02X%02X", tmp.R, tmp.G, tmp.B); ws2812_setColor(0,rgb);