diff --git a/code/espurna/api.ino b/code/espurna/api.ino new file mode 100644 index 00000000..4dc9e259 --- /dev/null +++ b/code/espurna/api.ino @@ -0,0 +1,185 @@ +/* + +API MODULE + +Copyright (C) 2016-2017 by Xose Pérez + +*/ + +#if WEB_SUPPORT + +#include +#include +#include +#include + +Ticker _api_defer; +typedef struct { + char * url; + char * key; + apiGetCallbackFunction getFn = NULL; + apiPutCallbackFunction putFn = NULL; +} web_api_t; +std::vector _apis; + +// ----------------------------------------------------------------------------- +// API +// ----------------------------------------------------------------------------- + +bool _authAPI(AsyncWebServerRequest *request) { + + if (getSetting("apiEnabled", API_ENABLED).toInt() == 0) { + DEBUG_MSG_P(PSTR("[WEBSERVER] HTTP API is not enabled\n")); + request->send(403); + return false; + } + + if (!request->hasParam("apikey", (request->method() == HTTP_PUT))) { + DEBUG_MSG_P(PSTR("[WEBSERVER] Missing apikey parameter\n")); + request->send(403); + return false; + } + + AsyncWebParameter* p = request->getParam("apikey", (request->method() == HTTP_PUT)); + if (!p->value().equals(getSetting("apiKey"))) { + DEBUG_MSG_P(PSTR("[WEBSERVER] Wrong apikey parameter\n")); + request->send(403); + return false; + } + + return true; + +} + +bool _asJson(AsyncWebServerRequest *request) { + bool asJson = false; + if (request->hasHeader("Accept")) { + AsyncWebHeader* h = request->getHeader("Accept"); + asJson = h->value().equals("application/json"); + } + return asJson; +} + +ArRequestHandlerFunction _bindAPI(unsigned int apiID) { + + return [apiID](AsyncWebServerRequest *request) { + + webLog(request); + if (!_authAPI(request)) return; + + web_api_t api = _apis[apiID]; + + // Check if its a PUT + if (api.putFn != NULL) { + if (request->hasParam("value", request->method() == HTTP_PUT)) { + AsyncWebParameter* p = request->getParam("value", request->method() == HTTP_PUT); + (api.putFn)((p->value()).c_str()); + } + } + + // Get response from callback + char value[API_BUFFER_SIZE]; + (api.getFn)(value, API_BUFFER_SIZE); + + // The response will be a 404 NOT FOUND if the resource is not available + if (!value) { + DEBUG_MSG_P(PSTR("[API] Sending 404 response\n")); + request->send(404); + return; + } + DEBUG_MSG_P(PSTR("[API] Sending response '%s'\n"), value); + + // Format response according to the Accept header + if (_asJson(request)) { + char buffer[64]; + snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": %s }"), api.key, value); + request->send(200, "application/json", buffer); + } else { + request->send(200, "text/plain", value); + } + + }; + +} + +void _onAPIs(AsyncWebServerRequest *request) { + + webLog(request); + if (!_authAPI(request)) return; + + bool asJson = _asJson(request); + + String output; + if (asJson) { + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + for (unsigned int i=0; i < _apis.size(); i++) { + root[_apis[i].key] = _apis[i].url; + } + root.printTo(output); + request->send(200, "application/json", output); + + } else { + for (unsigned int i=0; i < _apis.size(); i++) { + output += _apis[i].key + String(" -> ") + _apis[i].url + String("\n"); + } + request->send(200, "text/plain", output); + } + +} + +void _onRPC(AsyncWebServerRequest *request) { + + webLog(request); + if (!_authAPI(request)) return; + + //bool asJson = _asJson(request); + int response = 404; + + if (request->hasParam("action")) { + + AsyncWebParameter* p = request->getParam("action"); + String action = p->value(); + DEBUG_MSG_P(PSTR("[RPC] Action: %s\n"), action.c_str()); + + if (action.equals("reset")) { + response = 200; + _api_defer.once_ms(100, []() { + customReset(CUSTOM_RESET_RPC); + ESP.restart(); + }); + } + + } + + request->send(response); + +} + +// ----------------------------------------------------------------------------- + +void apiRegister(const char * url, const char * key, apiGetCallbackFunction getFn, apiPutCallbackFunction putFn) { + + // Store it + web_api_t api; + char buffer[40]; + snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), url); + api.url = strdup(buffer); + api.key = strdup(key); + api.getFn = getFn; + api.putFn = putFn; + _apis.push_back(api); + + // Bind call + unsigned int methods = HTTP_GET; + if (putFn != NULL) methods += HTTP_PUT; + webServer()->on(buffer, methods, _bindAPI(_apis.size() - 1)); + +} + +void apiSetup() { + webServer()->on("/apis", HTTP_GET, _onAPIs); + webServer()->on("/rpc", HTTP_GET, _onRPC); +} + +#endif // WEB_SUPPORT diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index 5437b361..620ae2a9 100644 --- a/code/espurna/config/prototypes.h +++ b/code/espurna/config/prototypes.h @@ -4,6 +4,8 @@ #include #include +AsyncWebServer * webServer(); + typedef std::function apiGetCallbackFunction; typedef std::function apiPutCallbackFunction; void apiRegister(const char * url, const char * key, apiGetCallbackFunction getFn, apiPutCallbackFunction putFn = NULL); diff --git a/code/espurna/espurna.ino b/code/espurna/espurna.ino index 7ec4a284..e8b26718 100644 --- a/code/espurna/espurna.ino +++ b/code/espurna/espurna.ino @@ -256,6 +256,8 @@ void setup() { // Init webserver required before any module that uses API #if WEB_SUPPORT webSetup(); + wsSetup(); + apiSetup(); #endif #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE diff --git a/code/espurna/web.ino b/code/espurna/web.ino index 46e33a7b..27a56275 100644 --- a/code/espurna/web.ino +++ b/code/espurna/web.ino @@ -15,8 +15,6 @@ Copyright (C) 2016-2017 by Xose Pérez #include #include #include -#include -#include "web.h" #if WEB_EMBEDDED #include "static/index.html.gz.h" @@ -34,928 +32,12 @@ char _last_modified[50]; Ticker _web_defer; // ----------------------------------------------------------------------------- - -AsyncWebSocket _ws("/ws"); - -// ----------------------------------------------------------------------------- - -typedef struct { - char * url; - char * key; - apiGetCallbackFunction getFn = NULL; - apiPutCallbackFunction putFn = NULL; -} web_api_t; -std::vector _apis; - -// ----------------------------------------------------------------------------- -// WEBSOCKETS -// ----------------------------------------------------------------------------- - -void _wsMQTTCallback(unsigned int type, const char * topic, const char * payload) { - - if (type == MQTT_CONNECT_EVENT) { - wsSend_P(PSTR("{\"mqttStatus\": true}")); - } - - if (type == MQTT_DISCONNECT_EVENT) { - wsSend_P(PSTR("{\"mqttStatus\": false}")); - } - -} - -void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { - - //DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing: %s\n"), length ? (char*) payload : ""); - - // Get client ID - uint32_t client_id = client->id(); - - // Parse JSON input - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.parseObject((char *) payload); - if (!root.success()) { - DEBUG_MSG_P(PSTR("[WEBSOCKET] Error parsing data\n")); - wsSend_P(client_id, PSTR("{\"message\": 3}")); - return; - } - - // Check actions - if (root.containsKey("action")) { - - String action = root["action"]; - - DEBUG_MSG_P(PSTR("[WEBSOCKET] Requested action: %s\n"), action.c_str()); - - if (action.equals("reset")) { - customReset(CUSTOM_RESET_WEB); - ESP.restart(); - } - - #ifdef ITEAD_SONOFF_RFBRIDGE - if (action.equals("rfblearn") && root.containsKey("data")) { - JsonObject& data = root["data"]; - rfbLearn(data["id"], data["status"]); - } - if (action.equals("rfbforget") && root.containsKey("data")) { - JsonObject& data = root["data"]; - rfbForget(data["id"], data["status"]); - } - if (action.equals("rfbsend") && root.containsKey("data")) { - JsonObject& data = root["data"]; - rfbStore(data["id"], data["status"], data["data"].as()); - } - #endif - - if (action.equals("restore") && root.containsKey("data")) { - - JsonObject& data = root["data"]; - if (!data.containsKey("app") || (data["app"] != APP_NAME)) { - wsSend_P(client_id, PSTR("{\"message\": 4}")); - return; - } - - for (unsigned int i = EEPROM_DATA_END; i < SPI_FLASH_SEC_SIZE; i++) { - EEPROM.write(i, 0xFF); - } - - for (auto element : data) { - if (strcmp(element.key, "app") == 0) continue; - if (strcmp(element.key, "version") == 0) continue; - setSetting(element.key, element.value.as()); - } - - saveSettings(); - - wsSend_P(client_id, PSTR("{\"message\": 5}")); - - } - - if (action.equals("reconnect")) { - - // Let the HTTP request return and disconnect after 100ms - _web_defer.once_ms(100, wifiDisconnect); - - } - - if (action.equals("relay") && root.containsKey("data")) { - - JsonObject& data = root["data"]; - - if (data.containsKey("status")) { - - unsigned char value = relayParsePayload(data["status"]); - - if (value == 3) { - - relayWS(); - - } else if (value < 3) { - - unsigned int relayID = 0; - if (data.containsKey("id")) { - String value = data["id"]; - relayID = value.toInt(); - } - - // Action to perform - if (value == 0) { - relayStatus(relayID, false); - } else if (value == 1) { - relayStatus(relayID, true); - } else if (value == 2) { - relayToggle(relayID); - } - - } - - } - - } - - #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE - - if (lightHasColor()) { - - if (action.equals("rgb") && root.containsKey("data")) { - lightColor((const char *) root["data"], true); - lightUpdate(true, true); - } - - if (action.equals("brightness") && root.containsKey("data")) { - lightBrightness(root["data"]); - lightUpdate(true, true); - } - - if (action.equals("hsv") && root.containsKey("data")) { - lightColor((const char *) root["data"], false); - lightUpdate(true, true); - } - - } - - if (action.equals("channel") && root.containsKey("data")) { - JsonObject& data = root["data"]; - if (data.containsKey("id") && data.containsKey("value")) { - lightChannel(data["id"], data["value"]); - lightUpdate(true, true); - } - } - - #ifdef LIGHT_PROVIDER_EXPERIMENTAL_RGB_ONLY_HSV_IR - if (action.equals("anim_mode") && root.containsKey("data")) { - lightAnimMode(root["data"]); - lightUpdate(true, true); - } - if (action.equals("anim_speed") && root.containsKey("data")) { - lightAnimSpeed(root["data"]); - lightUpdate(true, true); - } - #endif //LIGHT_PROVIDER_EXPERIMENTAL_RGB_ONLY_HSV_IR - - #endif //LIGHT_PROVIDER != LIGHT_PROVIDER_NONE - - }; - - // Check config - if (root.containsKey("config") && root["config"].is()) { - - JsonArray& config = root["config"]; - DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing configuration data\n")); - - unsigned char webMode = WEB_MODE_NORMAL; - - bool save = false; - bool changed = false; - bool changedMQTT = false; - bool changedNTP = false; - - unsigned int network = 0; - unsigned int dczRelayIdx = 0; - String adminPass; - - for (unsigned int i=0; i= relayCount()) continue; - key = key + String(dczRelayIdx); - ++dczRelayIdx; - } - - #else - - if (key.startsWith("dcz")) continue; - - #endif - - // Web portions - if (key == "webPort") { - if ((value.toInt() == 0) || (value.toInt() == 80)) { - save = changed = true; - delSetting(key); - continue; - } - } - - if (key == "webMode") { - webMode = value.toInt(); - continue; - } - - // Check password - if (key == "adminPass1") { - adminPass = value; - continue; - } - if (key == "adminPass2") { - if (!value.equals(adminPass)) { - wsSend_P(client_id, PSTR("{\"message\": 7}")); - return; - } - if (value.length() == 0) continue; - wsSend_P(client_id, PSTR("{\"action\": \"reload\"}")); - key = String("adminPass"); - } - - if (key == "ssid") { - key = key + String(network); - } - if (key == "pass") { - key = key + String(network); - } - if (key == "ip") { - key = key + String(network); - } - if (key == "gw") { - key = key + String(network); - } - if (key == "mask") { - key = key + String(network); - } - if (key == "dns") { - key = key + String(network); - ++network; - } - - if (value != getSetting(key)) { - //DEBUG_MSG_P(PSTR("[WEBSOCKET] Storing %s = %s\n", key.c_str(), value.c_str())); - setSetting(key, value); - save = changed = true; - if (key.startsWith("mqtt")) changedMQTT = true; - #if NTP_SUPPORT - if (key.startsWith("ntp")) changedNTP = true; - #endif - } - - } - - if (webMode == WEB_MODE_NORMAL) { - - // Clean wifi networks - int i = 0; - while (i < network) { - if (!hasSetting("ssid", i)) { - delSetting("ssid", i); - break; - } - if (!hasSetting("pass", i)) delSetting("pass", i); - if (!hasSetting("ip", i)) delSetting("ip", i); - if (!hasSetting("gw", i)) delSetting("gw", i); - if (!hasSetting("mask", i)) delSetting("mask", i); - if (!hasSetting("dns", i)) delSetting("dns", i); - ++i; - } - while (i < WIFI_MAX_NETWORKS) { - if (hasSetting("ssid", i)) { - save = changed = true; - } - delSetting("ssid", i); - delSetting("pass", i); - delSetting("ip", i); - delSetting("gw", i); - delSetting("mask", i); - delSetting("dns", i); - ++i; - } - - } - - // Save settings - if (save) { - - wsConfigure(); - saveSettings(); - wifiConfigure(); - otaConfigure(); - if (changedMQTT) { - mqttConfigure(); - mqttDisconnect(); - } - - #if ALEXA_SUPPORT - alexaConfigure(); - #endif - #if INFLUXDB_SUPPORT - influxDBConfigure(); - #endif - #if DOMOTICZ_SUPPORT - domoticzConfigure(); - #endif - #if NOFUSS_SUPPORT - nofussConfigure(); - #endif - #if RF_SUPPORT - rfBuildCodes(); - #endif - #if POWER_PROVIDER != POWER_PROVIDER_NONE - powerConfigure(); - #endif - #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE - #if LIGHT_SAVE_ENABLED == 0 - lightSave(); - #endif - #endif - #if NTP_SUPPORT - if (changedNTP) ntpConfigure(); - #endif - #if HOMEASSISTANT_SUPPORT - haConfigure(); - #endif - - } - - if (changed) { - wsSend_P(client_id, PSTR("{\"message\": 8}")); - } else { - wsSend_P(client_id, PSTR("{\"message\": 9}")); - } - - } - -} - -void _wsStart(uint32_t client_id) { - - char chipid[7]; - snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId()); - - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); - - bool changePassword = false; - #if WEB_FORCE_PASS_CHANGE - String adminPass = getSetting("adminPass", ADMIN_PASS); - if (adminPass.equals(ADMIN_PASS)) changePassword = true; - #endif - - if (changePassword) { - - root["webMode"] = WEB_MODE_PASSWORD; - - } else { - - root["webMode"] = WEB_MODE_NORMAL; - - root["app_name"] = APP_NAME; - root["app_version"] = APP_VERSION; - root["app_build"] = buildTime(); - root["manufacturer"] = MANUFACTURER; - root["chipid"] = chipid; - root["mac"] = WiFi.macAddress(); - root["device"] = DEVICE; - root["hostname"] = getSetting("hostname"); - root["network"] = getNetwork(); - root["deviceip"] = getIP(); - root["time"] = ntpDateTime(); - root["uptime"] = getUptime(); - root["heap"] = ESP.getFreeHeap(); - root["sketch_size"] = ESP.getSketchSize(); - root["free_size"] = ESP.getFreeSketchSpace(); - - #if NTP_SUPPORT - root["ntpVisible"] = 1; - root["ntpStatus"] = ntpConnected(); - root["ntpServer1"] = getSetting("ntpServer1", NTP_SERVER); - root["ntpServer2"] = getSetting("ntpServer2"); - root["ntpServer3"] = getSetting("ntpServer3"); - root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET).toInt(); - root["ntpDST"] = getSetting("ntpDST", NTP_DAY_LIGHT).toInt() == 1; - #endif - - root["mqttStatus"] = mqttConnected(); - root["mqttEnabled"] = mqttEnabled(); - root["mqttServer"] = getSetting("mqttServer", MQTT_SERVER); - root["mqttPort"] = getSetting("mqttPort", MQTT_PORT); - root["mqttUser"] = getSetting("mqttUser"); - root["mqttPassword"] = getSetting("mqttPassword"); - #if ASYNC_TCP_SSL_ENABLED - root["mqttsslVisible"] = 1; - root["mqttUseSSL"] = getSetting("mqttUseSSL", 0).toInt() == 1; - root["mqttFP"] = getSetting("mqttFP"); - #endif - root["mqttTopic"] = getSetting("mqttTopic", MQTT_TOPIC); - root["mqttUseJson"] = getSetting("mqttUseJson", MQTT_USE_JSON).toInt() == 1; - - JsonArray& relay = root.createNestedArray("relayStatus"); - for (unsigned char relayID=0; relayID 1) { - root["multirelayVisible"] = 1; - root["relaySync"] = getSetting("relaySync", RELAY_SYNC); - } - - root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt(); - - root["webPort"] = getSetting("webPort", WEB_PORT).toInt(); - - root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1; - root["apiKey"] = getSetting("apiKey"); - root["apiRealTime"] = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1; - - root["tmpUnits"] = getSetting("tmpUnits", TMP_UNITS).toInt(); - - #if HOMEASSISTANT_SUPPORT - root["haVisible"] = 1; - root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX); - #endif // HOMEASSISTANT_SUPPORT - - #if DOMOTICZ_SUPPORT - - root["dczVisible"] = 1; - root["dczEnabled"] = getSetting("dczEnabled", DOMOTICZ_ENABLED).toInt() == 1; - root["dczSkip"] = getSetting("dczSkip", DOMOTICZ_SKIP_TIME); - root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC); - root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC); - - JsonArray& dczRelayIdx = root.createNestedArray("dczRelayIdx"); - for (byte i=0; iremoteIP(); - DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url()); - _wsStart(client->id()); - client->_tempObject = new WebSocketIncommingBuffer(&_wsParse, true); - wifiReconnectCheck(); - - } else if(type == WS_EVT_DISCONNECT) { - DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u disconnected\n"), client->id()); - if (client->_tempObject) { - delete (WebSocketIncommingBuffer *) client->_tempObject; - } - wifiReconnectCheck(); - - } else if(type == WS_EVT_ERROR) { - DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u error(%u): %s\n"), client->id(), *((uint16_t*)arg), (char*)data); - - } else if(type == WS_EVT_PONG) { - DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u pong(%u): %s\n"), client->id(), len, len ? (char*) data : ""); - - } else if(type == WS_EVT_DATA) { - //DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u data(%u): %s\n"), client->id(), len, len ? (char*) data : ""); - WebSocketIncommingBuffer *buffer = (WebSocketIncommingBuffer *)client->_tempObject; - AwsFrameInfo * info = (AwsFrameInfo*)arg; - buffer->data_event(client, info, data, len); - - } - - -} - -// ----------------------------------------------------------------------------- - -bool wsConnected() { - return (_ws.count() > 0); -} - -void wsSend(const char * payload) { - if (_ws.count() > 0) { - _ws.textAll(payload); - } -} - -void wsSend_P(PGM_P payload) { - if (_ws.count() > 0) { - char buffer[strlen_P(payload)]; - strcpy_P(buffer, payload); - _ws.textAll(buffer); - } -} - -void wsSend(uint32_t client_id, const char * payload) { - _ws.text(client_id, payload); -} - -void wsSend_P(uint32_t client_id, PGM_P payload) { - char buffer[strlen_P(payload)]; - strcpy_P(buffer, payload); - _ws.text(client_id, buffer); -} - -void wsConfigure() { - _ws.setAuthentication(WEB_USERNAME, (const char *) getSetting("adminPass", ADMIN_PASS).c_str()); -} - -void wsSetup() { - _ws.onEvent(_wsEvent); - wsConfigure(); - _server->addHandler(&_ws); - mqttRegister(_wsMQTTCallback); -} - -// ----------------------------------------------------------------------------- -// API +// HOOKS // ----------------------------------------------------------------------------- -bool _authAPI(AsyncWebServerRequest *request) { - - if (getSetting("apiEnabled", API_ENABLED).toInt() == 0) { - DEBUG_MSG_P(PSTR("[WEBSERVER] HTTP API is not enabled\n")); - request->send(403); - return false; - } - - if (!request->hasParam("apikey", (request->method() == HTTP_PUT))) { - DEBUG_MSG_P(PSTR("[WEBSERVER] Missing apikey parameter\n")); - request->send(403); - return false; - } - - AsyncWebParameter* p = request->getParam("apikey", (request->method() == HTTP_PUT)); - if (!p->value().equals(getSetting("apiKey"))) { - DEBUG_MSG_P(PSTR("[WEBSERVER] Wrong apikey parameter\n")); - request->send(403); - return false; - } - - return true; - -} - -bool _asJson(AsyncWebServerRequest *request) { - bool asJson = false; - if (request->hasHeader("Accept")) { - AsyncWebHeader* h = request->getHeader("Accept"); - asJson = h->value().equals("application/json"); - } - return asJson; -} - -ArRequestHandlerFunction _bindAPI(unsigned int apiID) { - - return [apiID](AsyncWebServerRequest *request) { - - _webLog(request); - if (!_authAPI(request)) return; - - web_api_t api = _apis[apiID]; - - // Check if its a PUT - if (api.putFn != NULL) { - if (request->hasParam("value", request->method() == HTTP_PUT)) { - AsyncWebParameter* p = request->getParam("value", request->method() == HTTP_PUT); - (api.putFn)((p->value()).c_str()); - } - } - - // Get response from callback - char value[API_BUFFER_SIZE]; - (api.getFn)(value, API_BUFFER_SIZE); - - // The response will be a 404 NOT FOUND if the resource is not available - if (!value) { - DEBUG_MSG_P(PSTR("[API] Sending 404 response\n")); - request->send(404); - return; - } - DEBUG_MSG_P(PSTR("[API] Sending response '%s'\n"), value); - - // Format response according to the Accept header - if (_asJson(request)) { - char buffer[64]; - snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": %s }"), api.key, value); - request->send(200, "application/json", buffer); - } else { - request->send(200, "text/plain", value); - } - - }; - -} - -void _onAPIs(AsyncWebServerRequest *request) { - - _webLog(request); - if (!_authAPI(request)) return; - - bool asJson = _asJson(request); - - String output; - if (asJson) { - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); - for (unsigned int i=0; i < _apis.size(); i++) { - root[_apis[i].key] = _apis[i].url; - } - root.printTo(output); - request->send(200, "application/json", output); - - } else { - for (unsigned int i=0; i < _apis.size(); i++) { - output += _apis[i].key + String(" -> ") + _apis[i].url + String("\n"); - } - request->send(200, "text/plain", output); - } - -} - -void _onRPC(AsyncWebServerRequest *request) { - - _webLog(request); - if (!_authAPI(request)) return; - - //bool asJson = _asJson(request); - int response = 404; - - if (request->hasParam("action")) { - - AsyncWebParameter* p = request->getParam("action"); - String action = p->value(); - DEBUG_MSG_P(PSTR("[RPC] Action: %s\n"), action.c_str()); - - if (action.equals("reset")) { - response = 200; - _web_defer.once_ms(100, []() { - customReset(CUSTOM_RESET_RPC); - ESP.restart(); - }); - } - - } - - request->send(response); - -} - -// ----------------------------------------------------------------------------- - -void apiRegister(const char * url, const char * key, apiGetCallbackFunction getFn, apiPutCallbackFunction putFn) { - - // Store it - web_api_t api; - char buffer[40]; - snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), url); - api.url = strdup(buffer); - api.key = strdup(key); - api.getFn = getFn; - api.putFn = putFn; - _apis.push_back(api); - - // Bind call - unsigned int methods = HTTP_GET; - if (putFn != NULL) methods += HTTP_PUT; - _server->on(buffer, methods, _bindAPI(_apis.size() - 1)); - -} - -void apiSetup() { - _server->on("/apis", HTTP_GET, _onAPIs); - _server->on("/rpc", HTTP_GET, _onRPC); -} - -// ----------------------------------------------------------------------------- -// WEBSERVER -// ----------------------------------------------------------------------------- - -void _webLog(AsyncWebServerRequest *request) { - DEBUG_MSG_P(PSTR("[WEBSERVER] Request: %s %s\n"), request->methodToString(), request->url().c_str()); -} - -bool _authenticate(AsyncWebServerRequest *request) { - String password = getSetting("adminPass", ADMIN_PASS); - char httpPassword[password.length() + 1]; - password.toCharArray(httpPassword, password.length() + 1); - return request->authenticate(WEB_USERNAME, httpPassword); -} - void _onGetConfig(AsyncWebServerRequest *request) { - _webLog(request); + webLog(request); if (!_authenticate(request)) return request->requestAuthentication(); AsyncJsonResponse * response = new AsyncJsonResponse(); @@ -982,7 +64,7 @@ void _onGetConfig(AsyncWebServerRequest *request) { #if WEB_EMBEDDED void _onHome(AsyncWebServerRequest *request) { - _webLog(request); + webLog(request); if (!_authenticate(request)) return request->requestAuthentication(); if (request->header("If-Modified-Since").equals(_last_modified)) { @@ -1082,7 +164,7 @@ int _onCertificate(void * arg, const char *filename, uint8_t **buf) { void _onUpgrade(AsyncWebServerRequest *request) { - _webLog(request); + webLog(request); if (!_authenticate(request)) return request->requestAuthentication(); char buffer[10]; @@ -1136,6 +218,23 @@ void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t inde // ----------------------------------------------------------------------------- +bool _authenticate(AsyncWebServerRequest *request) { + String password = getSetting("adminPass", ADMIN_PASS); + char httpPassword[password.length() + 1]; + password.toCharArray(httpPassword, password.length() + 1); + return request->authenticate(WEB_USERNAME, httpPassword); +} + +// ----------------------------------------------------------------------------- + +AsyncWebServer * webServer() { + return _server; +} + +void webLog(AsyncWebServerRequest *request) { + DEBUG_MSG_P(PSTR("[WEBSERVER] Request: %s %s\n"), request->methodToString(), request->url().c_str()); +} + void webSetup() { // Cache the Last-Modifier header value @@ -1149,12 +248,6 @@ void webSetup() { #endif _server = new AsyncWebServer(port); - // Setup websocket - wsSetup(); - - // API setup - apiSetup(); - // Rewrites _server->rewrite("/", "/index.html"); @@ -1170,7 +263,7 @@ void webSetup() { _server->serveStatic("/", SPIFFS, "/") .setLastModified(_last_modified) .setFilter([](AsyncWebServerRequest *request) -> bool { - _webLog(request); + webLog(request); return true; }); #endif diff --git a/code/espurna/web.h b/code/espurna/ws.h similarity index 100% rename from code/espurna/web.h rename to code/espurna/ws.h diff --git a/code/espurna/ws.ino b/code/espurna/ws.ino new file mode 100644 index 00000000..24ed8923 --- /dev/null +++ b/code/espurna/ws.ino @@ -0,0 +1,751 @@ +/* + +WEBSOCKET MODULE + +Copyright (C) 2016-2017 by Xose Pérez + +*/ + +#if WEB_SUPPORT + +#include +#include +#include +#include "ws.h" + +AsyncWebSocket _ws("/ws"); + +// ----------------------------------------------------------------------------- +// Private methods +// ----------------------------------------------------------------------------- + +void _wsMQTTCallback(unsigned int type, const char * topic, const char * payload) { + + if (type == MQTT_CONNECT_EVENT) { + wsSend_P(PSTR("{\"mqttStatus\": true}")); + } + + if (type == MQTT_DISCONNECT_EVENT) { + wsSend_P(PSTR("{\"mqttStatus\": false}")); + } + +} + +void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { + + //DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing: %s\n"), length ? (char*) payload : ""); + + // Get client ID + uint32_t client_id = client->id(); + + // Parse JSON input + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.parseObject((char *) payload); + if (!root.success()) { + DEBUG_MSG_P(PSTR("[WEBSOCKET] Error parsing data\n")); + wsSend_P(client_id, PSTR("{\"message\": 3}")); + return; + } + + // Check actions + if (root.containsKey("action")) { + + String action = root["action"]; + + DEBUG_MSG_P(PSTR("[WEBSOCKET] Requested action: %s\n"), action.c_str()); + + if (action.equals("reset")) { + customReset(CUSTOM_RESET_WEB); + ESP.restart(); + } + + #ifdef ITEAD_SONOFF_RFBRIDGE + if (action.equals("rfblearn") && root.containsKey("data")) { + JsonObject& data = root["data"]; + rfbLearn(data["id"], data["status"]); + } + if (action.equals("rfbforget") && root.containsKey("data")) { + JsonObject& data = root["data"]; + rfbForget(data["id"], data["status"]); + } + if (action.equals("rfbsend") && root.containsKey("data")) { + JsonObject& data = root["data"]; + rfbStore(data["id"], data["status"], data["data"].as()); + } + #endif + + if (action.equals("restore") && root.containsKey("data")) { + + JsonObject& data = root["data"]; + if (!data.containsKey("app") || (data["app"] != APP_NAME)) { + wsSend_P(client_id, PSTR("{\"message\": 4}")); + return; + } + + for (unsigned int i = EEPROM_DATA_END; i < SPI_FLASH_SEC_SIZE; i++) { + EEPROM.write(i, 0xFF); + } + + for (auto element : data) { + if (strcmp(element.key, "app") == 0) continue; + if (strcmp(element.key, "version") == 0) continue; + setSetting(element.key, element.value.as()); + } + + saveSettings(); + + wsSend_P(client_id, PSTR("{\"message\": 5}")); + + } + + if (action.equals("reconnect")) { + + // Let the HTTP request return and disconnect after 100ms + _web_defer.once_ms(100, wifiDisconnect); + + } + + if (action.equals("relay") && root.containsKey("data")) { + + JsonObject& data = root["data"]; + + if (data.containsKey("status")) { + + unsigned char value = relayParsePayload(data["status"]); + + if (value == 3) { + + relayWS(); + + } else if (value < 3) { + + unsigned int relayID = 0; + if (data.containsKey("id")) { + String value = data["id"]; + relayID = value.toInt(); + } + + // Action to perform + if (value == 0) { + relayStatus(relayID, false); + } else if (value == 1) { + relayStatus(relayID, true); + } else if (value == 2) { + relayToggle(relayID); + } + + } + + } + + } + + #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE + + if (lightHasColor()) { + + if (action.equals("rgb") && root.containsKey("data")) { + lightColor((const char *) root["data"], true); + lightUpdate(true, true); + } + + if (action.equals("brightness") && root.containsKey("data")) { + lightBrightness(root["data"]); + lightUpdate(true, true); + } + + if (action.equals("hsv") && root.containsKey("data")) { + lightColor((const char *) root["data"], false); + lightUpdate(true, true); + } + + } + + if (action.equals("channel") && root.containsKey("data")) { + JsonObject& data = root["data"]; + if (data.containsKey("id") && data.containsKey("value")) { + lightChannel(data["id"], data["value"]); + lightUpdate(true, true); + } + } + + #ifdef LIGHT_PROVIDER_EXPERIMENTAL_RGB_ONLY_HSV_IR + if (action.equals("anim_mode") && root.containsKey("data")) { + lightAnimMode(root["data"]); + lightUpdate(true, true); + } + if (action.equals("anim_speed") && root.containsKey("data")) { + lightAnimSpeed(root["data"]); + lightUpdate(true, true); + } + #endif //LIGHT_PROVIDER_EXPERIMENTAL_RGB_ONLY_HSV_IR + + #endif //LIGHT_PROVIDER != LIGHT_PROVIDER_NONE + + }; + + // Check config + if (root.containsKey("config") && root["config"].is()) { + + JsonArray& config = root["config"]; + DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing configuration data\n")); + + unsigned char webMode = WEB_MODE_NORMAL; + + bool save = false; + bool changed = false; + bool changedMQTT = false; + bool changedNTP = false; + + unsigned int network = 0; + unsigned int dczRelayIdx = 0; + String adminPass; + + for (unsigned int i=0; i= relayCount()) continue; + key = key + String(dczRelayIdx); + ++dczRelayIdx; + } + + #else + + if (key.startsWith("dcz")) continue; + + #endif + + // Web portions + if (key == "webPort") { + if ((value.toInt() == 0) || (value.toInt() == 80)) { + save = changed = true; + delSetting(key); + continue; + } + } + + if (key == "webMode") { + webMode = value.toInt(); + continue; + } + + // Check password + if (key == "adminPass1") { + adminPass = value; + continue; + } + if (key == "adminPass2") { + if (!value.equals(adminPass)) { + wsSend_P(client_id, PSTR("{\"message\": 7}")); + return; + } + if (value.length() == 0) continue; + wsSend_P(client_id, PSTR("{\"action\": \"reload\"}")); + key = String("adminPass"); + } + + if (key == "ssid") { + key = key + String(network); + } + if (key == "pass") { + key = key + String(network); + } + if (key == "ip") { + key = key + String(network); + } + if (key == "gw") { + key = key + String(network); + } + if (key == "mask") { + key = key + String(network); + } + if (key == "dns") { + key = key + String(network); + ++network; + } + + if (value != getSetting(key)) { + //DEBUG_MSG_P(PSTR("[WEBSOCKET] Storing %s = %s\n", key.c_str(), value.c_str())); + setSetting(key, value); + save = changed = true; + if (key.startsWith("mqtt")) changedMQTT = true; + #if NTP_SUPPORT + if (key.startsWith("ntp")) changedNTP = true; + #endif + } + + } + + if (webMode == WEB_MODE_NORMAL) { + + // Clean wifi networks + int i = 0; + while (i < network) { + if (!hasSetting("ssid", i)) { + delSetting("ssid", i); + break; + } + if (!hasSetting("pass", i)) delSetting("pass", i); + if (!hasSetting("ip", i)) delSetting("ip", i); + if (!hasSetting("gw", i)) delSetting("gw", i); + if (!hasSetting("mask", i)) delSetting("mask", i); + if (!hasSetting("dns", i)) delSetting("dns", i); + ++i; + } + while (i < WIFI_MAX_NETWORKS) { + if (hasSetting("ssid", i)) { + save = changed = true; + } + delSetting("ssid", i); + delSetting("pass", i); + delSetting("ip", i); + delSetting("gw", i); + delSetting("mask", i); + delSetting("dns", i); + ++i; + } + + } + + // Save settings + if (save) { + + wsConfigure(); + saveSettings(); + wifiConfigure(); + otaConfigure(); + if (changedMQTT) { + mqttConfigure(); + mqttDisconnect(); + } + + #if ALEXA_SUPPORT + alexaConfigure(); + #endif + #if INFLUXDB_SUPPORT + influxDBConfigure(); + #endif + #if DOMOTICZ_SUPPORT + domoticzConfigure(); + #endif + #if NOFUSS_SUPPORT + nofussConfigure(); + #endif + #if RF_SUPPORT + rfBuildCodes(); + #endif + #if POWER_PROVIDER != POWER_PROVIDER_NONE + powerConfigure(); + #endif + #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE + #if LIGHT_SAVE_ENABLED == 0 + lightSave(); + #endif + #endif + #if NTP_SUPPORT + if (changedNTP) ntpConfigure(); + #endif + #if HOMEASSISTANT_SUPPORT + haConfigure(); + #endif + + } + + if (changed) { + wsSend_P(client_id, PSTR("{\"message\": 8}")); + } else { + wsSend_P(client_id, PSTR("{\"message\": 9}")); + } + + } + +} + +void _wsStart(uint32_t client_id) { + + char chipid[7]; + snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId()); + + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + + bool changePassword = false; + #if WEB_FORCE_PASS_CHANGE + String adminPass = getSetting("adminPass", ADMIN_PASS); + if (adminPass.equals(ADMIN_PASS)) changePassword = true; + #endif + + if (changePassword) { + + root["webMode"] = WEB_MODE_PASSWORD; + + } else { + + root["webMode"] = WEB_MODE_NORMAL; + + root["app_name"] = APP_NAME; + root["app_version"] = APP_VERSION; + root["app_build"] = buildTime(); + root["manufacturer"] = MANUFACTURER; + root["chipid"] = chipid; + root["mac"] = WiFi.macAddress(); + root["device"] = DEVICE; + root["hostname"] = getSetting("hostname"); + root["network"] = getNetwork(); + root["deviceip"] = getIP(); + root["time"] = ntpDateTime(); + root["uptime"] = getUptime(); + root["heap"] = ESP.getFreeHeap(); + root["sketch_size"] = ESP.getSketchSize(); + root["free_size"] = ESP.getFreeSketchSpace(); + + #if NTP_SUPPORT + root["ntpVisible"] = 1; + root["ntpStatus"] = ntpConnected(); + root["ntpServer1"] = getSetting("ntpServer1", NTP_SERVER); + root["ntpServer2"] = getSetting("ntpServer2"); + root["ntpServer3"] = getSetting("ntpServer3"); + root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET).toInt(); + root["ntpDST"] = getSetting("ntpDST", NTP_DAY_LIGHT).toInt() == 1; + #endif + + root["mqttStatus"] = mqttConnected(); + root["mqttEnabled"] = mqttEnabled(); + root["mqttServer"] = getSetting("mqttServer", MQTT_SERVER); + root["mqttPort"] = getSetting("mqttPort", MQTT_PORT); + root["mqttUser"] = getSetting("mqttUser"); + root["mqttPassword"] = getSetting("mqttPassword"); + #if ASYNC_TCP_SSL_ENABLED + root["mqttsslVisible"] = 1; + root["mqttUseSSL"] = getSetting("mqttUseSSL", 0).toInt() == 1; + root["mqttFP"] = getSetting("mqttFP"); + #endif + root["mqttTopic"] = getSetting("mqttTopic", MQTT_TOPIC); + root["mqttUseJson"] = getSetting("mqttUseJson", MQTT_USE_JSON).toInt() == 1; + + JsonArray& relay = root.createNestedArray("relayStatus"); + for (unsigned char relayID=0; relayID 1) { + root["multirelayVisible"] = 1; + root["relaySync"] = getSetting("relaySync", RELAY_SYNC); + } + + root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt(); + + root["webPort"] = getSetting("webPort", WEB_PORT).toInt(); + + root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1; + root["apiKey"] = getSetting("apiKey"); + root["apiRealTime"] = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1; + + root["tmpUnits"] = getSetting("tmpUnits", TMP_UNITS).toInt(); + + #if HOMEASSISTANT_SUPPORT + root["haVisible"] = 1; + root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX); + #endif // HOMEASSISTANT_SUPPORT + + #if DOMOTICZ_SUPPORT + + root["dczVisible"] = 1; + root["dczEnabled"] = getSetting("dczEnabled", DOMOTICZ_ENABLED).toInt() == 1; + root["dczSkip"] = getSetting("dczSkip", DOMOTICZ_SKIP_TIME); + root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC); + root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC); + + JsonArray& dczRelayIdx = root.createNestedArray("dczRelayIdx"); + for (byte i=0; iremoteIP(); + DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url()); + _wsStart(client->id()); + client->_tempObject = new WebSocketIncommingBuffer(&_wsParse, true); + wifiReconnectCheck(); + + } else if(type == WS_EVT_DISCONNECT) { + DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u disconnected\n"), client->id()); + if (client->_tempObject) { + delete (WebSocketIncommingBuffer *) client->_tempObject; + } + wifiReconnectCheck(); + + } else if(type == WS_EVT_ERROR) { + DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u error(%u): %s\n"), client->id(), *((uint16_t*)arg), (char*)data); + + } else if(type == WS_EVT_PONG) { + DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u pong(%u): %s\n"), client->id(), len, len ? (char*) data : ""); + + } else if(type == WS_EVT_DATA) { + //DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u data(%u): %s\n"), client->id(), len, len ? (char*) data : ""); + WebSocketIncommingBuffer *buffer = (WebSocketIncommingBuffer *)client->_tempObject; + AwsFrameInfo * info = (AwsFrameInfo*)arg; + buffer->data_event(client, info, data, len); + + } + + +} + +// ----------------------------------------------------------------------------- +// Piblic API +// ----------------------------------------------------------------------------- + +bool wsConnected() { + return (_ws.count() > 0); +} + +void wsSend(const char * payload) { + if (_ws.count() > 0) { + _ws.textAll(payload); + } +} + +void wsSend_P(PGM_P payload) { + if (_ws.count() > 0) { + char buffer[strlen_P(payload)]; + strcpy_P(buffer, payload); + _ws.textAll(buffer); + } +} + +void wsSend(uint32_t client_id, const char * payload) { + _ws.text(client_id, payload); +} + +void wsSend_P(uint32_t client_id, PGM_P payload) { + char buffer[strlen_P(payload)]; + strcpy_P(buffer, payload); + _ws.text(client_id, buffer); +} + +void wsConfigure() { + _ws.setAuthentication(WEB_USERNAME, (const char *) getSetting("adminPass", ADMIN_PASS).c_str()); +} + +void wsSetup() { + _ws.onEvent(_wsEvent); + wsConfigure(); + webServer()->addHandler(&_ws); + mqttRegister(_wsMQTTCallback); +} + +#endif // WEB_SUPPORT