diff --git a/code/espurna/ws.ino b/code/espurna/ws.ino index 6b334034..7fee9b2e 100644 --- a/code/espurna/ws.ino +++ b/code/espurna/ws.ino @@ -1,401 +1,401 @@ -/* - -WEBSOCKET MODULE - -Copyright (C) 2016-2018 by Xose Pérez - -*/ - -#if WEB_SUPPORT - -#include -#include -#include -#include -#include -#include "libs/WebSocketIncommingBuffer.h" - -AsyncWebSocket _ws("/ws"); -Ticker _web_defer; - -std::vector _ws_on_send_callbacks; -std::vector _ws_on_action_callbacks; -std::vector _ws_on_after_parse_callbacks; - -// ----------------------------------------------------------------------------- -// Private methods -// ----------------------------------------------------------------------------- - -#if MQTT_SUPPORT -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}")); -} -#endif - -bool _wsStore(String key, String value) { - - // HTTP port - if (key == "webPort") { - if ((value.toInt() == 0) || (value.toInt() == 80)) { - return delSetting(key); - } - } - - if (value != getSetting(key)) { - return setSetting(key, value); - } - - return false; - -} - -bool _wsStore(String key, JsonArray& value) { - - bool changed = false; - - unsigned char index = 0; - for (auto element : value) { - if (_wsStore(key + index, element.as())) changed = true; - index++; - } - - // Delete further values - for (unsigned char i=index; iid(); - - // 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 ----------------------------------------------------------- - - const char* action = root["action"]; - if (action) { - - DEBUG_MSG_P(PSTR("[WEBSOCKET] Requested action: %s\n"), action); - - if (strcmp(action, "reboot") == 0) deferredReset(100, CUSTOM_RESET_WEB); - if (strcmp(action, "reconnect") == 0) _web_defer.once_ms(100, wifiDisconnect); - - if (strcmp(action, "factory_reset") == 0) - { - DEBUG_MSG_P(PSTR("\n\nFACTORY RESET\n\n")); - resetSettings(); - deferredReset(100, CUSTOM_RESET_FACTORY); - } - - JsonObject& data = root["data"]; - if (data.success()) { - - // Callbacks - for (unsigned char i = 0; i < _ws_on_action_callbacks.size(); i++) { - (_ws_on_action_callbacks[i])(client_id, action, data); - } - - // Restore configuration via websockets - if (strcmp(action, "restore") == 0) { - if (settingsRestoreJson(data)) { - wsSend_P(client_id, PSTR("{\"message\": 5}")); - } else { - wsSend_P(client_id, PSTR("{\"message\": 4}")); - } - } - - } - - }; - - // Check configuration ----------------------------------------------------- - - JsonObject& config = root["config"]; - if (config.success()) { - - DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing configuration data\n")); - - String adminPass; - bool save = false; - #if MQTT_SUPPORT - bool changedMQTT = false; - #endif - - for (auto kv: config) { - - bool changed = false; - String key = kv.key; - JsonVariant& value = kv.value; - - // Check password - if (key == "adminPass") { - if (!value.is()) continue; - JsonArray& values = value.as(); - if (values.size() != 2) continue; - if (values[0].as().equals(values[1].as())) { - String password = values[0].as(); - if (password.length() > 0) { - setSetting(key, password); - save = true; - wsSend_P(client_id, PSTR("{\"action\": \"reload\"}")); - } - } else { - wsSend_P(client_id, PSTR("{\"message\": 7}")); - } - continue; - } - - // Store values - if (value.is()) { - if (_wsStore(key, value.as())) changed = true; - } else { - if (_wsStore(key, value.as())) changed = true; - } - - // Update flags if value has changed - if (changed) { - save = true; - #if MQTT_SUPPORT - if (key.startsWith("mqtt")) changedMQTT = true; - #endif - } - - } - - // Save settings - if (save) { - - // Callbacks - for (unsigned char i = 0; i < _ws_on_after_parse_callbacks.size(); i++) { - (_ws_on_after_parse_callbacks[i])(); - } - - // This should got to callback as well - // but first change management has to be in place - #if MQTT_SUPPORT - if (changedMQTT) mqttReset(); - #endif - - // Persist settings - saveSettings(); - - wsSend_P(client_id, PSTR("{\"message\": 8}")); - - } else { - - wsSend_P(client_id, PSTR("{\"message\": 9}")); - - } - - } - -} - -void _wsUpdate(JsonObject& root) { - root["heap"] = getFreeHeap(); - root["uptime"] = getUptime(); - root["rssi"] = WiFi.RSSI(); - root["distance"] = wifiDistance(WiFi.RSSI()); - #if NTP_SUPPORT - if (ntpSynced()) root["now"] = now(); - #endif -} - -void _wsOnStart(JsonObject& root) { - - #if USE_PASSWORD && WEB_FORCE_PASS_CHANGE - String adminPass = getSetting("adminPass", ADMIN_PASS); - bool changePassword = adminPass.equals(ADMIN_PASS); - #else - bool changePassword = false; - #endif - - if (changePassword) { - - root["webMode"] = WEB_MODE_PASSWORD; - - } else { - - char chipid[7]; - snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId()); - uint8_t * bssid = WiFi.BSSID(); - char bssid_str[20]; - snprintf_P(bssid_str, sizeof(bssid_str), - PSTR("%02X:%02X:%02X:%02X:%02X:%02X"), - bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5] - ); - - root["webMode"] = WEB_MODE_NORMAL; - - root["app_name"] = APP_NAME; - root["app_version"] = APP_VERSION; - root["app_build"] = buildTime(); - root["manufacturer"] = MANUFACTURER; - root["chipid"] = String(chipid); - root["mac"] = WiFi.macAddress(); - root["bssid"] = String(bssid_str); - root["channel"] = WiFi.channel(); - root["device"] = DEVICE; - root["hostname"] = getSetting("hostname"); - root["network"] = getNetwork(); - root["deviceip"] = getIP(); - root["sketch_size"] = ESP.getSketchSize(); - root["free_size"] = ESP.getFreeSketchSpace(); - - _wsUpdate(root); - - root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt(); - root["webPort"] = getSetting("webPort", WEB_PORT).toInt(); - root["tmpUnits"] = getSetting("tmpUnits", SENSOR_TEMPERATURE_UNITS).toInt(); - root["tmpCorrection"] = getSetting("tmpCorrection", SENSOR_TEMPERATURE_CORRECTION).toFloat(); - - } - -} - -void _wsStart(uint32_t client_id) { - for (unsigned char i = 0; i < _ws_on_send_callbacks.size(); i++) { - wsSend(client_id, _ws_on_send_callbacks[i]); - } -} - -void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ - - if (type == WS_EVT_CONNECT) { - IPAddress ip = client->remoteIP(); - 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); - - } - -} - -void _wsLoop() { - static unsigned long last = 0; - if (!wsConnected()) return; - if (millis() - last > WS_UPDATE_INTERVAL) { - last = millis(); - wsSend(_wsUpdate); - } -} - -// ----------------------------------------------------------------------------- -// Piblic API -// ----------------------------------------------------------------------------- - -bool wsConnected() { - return (_ws.count() > 0); -} - -void wsOnSendRegister(ws_on_send_callback_f callback) { - _ws_on_send_callbacks.push_back(callback); -} - -void wsOnActionRegister(ws_on_action_callback_f callback) { - _ws_on_action_callbacks.push_back(callback); -} - -void wsOnAfterParseRegister(ws_on_after_parse_callback_f callback) { - _ws_on_after_parse_callbacks.push_back(callback); -} - -void wsSend(ws_on_send_callback_f callback) { - if (_ws.count() > 0) { - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); - callback(root); - String output; - root.printTo(output); - _ws.textAll((char *) output.c_str()); - } -} - -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, ws_on_send_callback_f callback) { - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); - callback(root); - String output; - root.printTo(output); - _ws.text(client_id, (char *) output.c_str()); -} - -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() { - #if USE_PASSWORD - _ws.setAuthentication(WEB_USERNAME, (const char *) getSetting("adminPass", ADMIN_PASS).c_str()); - #endif -} - -void wsSetup() { - _ws.onEvent(_wsEvent); - wsConfigure(); - webServer()->addHandler(&_ws); - #if MQTT_SUPPORT - mqttRegister(_wsMQTTCallback); - #endif - wsOnSendRegister(_wsOnStart); - wsOnAfterParseRegister(wsConfigure); - espurnaRegisterLoop(_wsLoop); -} - -#endif // WEB_SUPPORT +/* + +WEBSOCKET MODULE + +Copyright (C) 2016-2018 by Xose Pérez + +*/ + +#if WEB_SUPPORT + +#include +#include +#include +#include +#include +#include "libs/WebSocketIncommingBuffer.h" + +AsyncWebSocket _ws("/ws"); +Ticker _web_defer; + +std::vector _ws_on_send_callbacks; +std::vector _ws_on_action_callbacks; +std::vector _ws_on_after_parse_callbacks; + +// ----------------------------------------------------------------------------- +// Private methods +// ----------------------------------------------------------------------------- + +#if MQTT_SUPPORT +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}")); +} +#endif + +bool _wsStore(String key, String value) { + + // HTTP port + if (key == "webPort") { + if ((value.toInt() == 0) || (value.toInt() == 80)) { + return delSetting(key); + } + } + + if (value != getSetting(key)) { + return setSetting(key, value); + } + + return false; + +} + +bool _wsStore(String key, JsonArray& value) { + + bool changed = false; + + unsigned char index = 0; + for (auto element : value) { + if (_wsStore(key + index, element.as())) changed = true; + index++; + } + + // Delete further values + for (unsigned char i=index; iid(); + + // 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 ----------------------------------------------------------- + + const char* action = root["action"]; + if (action) { + + DEBUG_MSG_P(PSTR("[WEBSOCKET] Requested action: %s\n"), action); + + if (strcmp(action, "reboot") == 0) deferredReset(100, CUSTOM_RESET_WEB); + if (strcmp(action, "reconnect") == 0) _web_defer.once_ms(100, wifiDisconnect); + + if (strcmp(action, "factory_reset") == 0) + { + DEBUG_MSG_P(PSTR("\n\nFACTORY RESET\n\n")); + resetSettings(); + deferredReset(100, CUSTOM_RESET_FACTORY); + } + + JsonObject& data = root["data"]; + if (data.success()) { + + // Callbacks + for (unsigned char i = 0; i < _ws_on_action_callbacks.size(); i++) { + (_ws_on_action_callbacks[i])(client_id, action, data); + } + + // Restore configuration via websockets + if (strcmp(action, "restore") == 0) { + if (settingsRestoreJson(data)) { + wsSend_P(client_id, PSTR("{\"message\": 5}")); + } else { + wsSend_P(client_id, PSTR("{\"message\": 4}")); + } + } + + } + + }; + + // Check configuration ----------------------------------------------------- + + JsonObject& config = root["config"]; + if (config.success()) { + + DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing configuration data\n")); + + String adminPass; + bool save = false; + #if MQTT_SUPPORT + bool changedMQTT = false; + #endif + + for (auto kv: config) { + + bool changed = false; + String key = kv.key; + JsonVariant& value = kv.value; + + // Check password + if (key == "adminPass") { + if (!value.is()) continue; + JsonArray& values = value.as(); + if (values.size() != 2) continue; + if (values[0].as().equals(values[1].as())) { + String password = values[0].as(); + if (password.length() > 0) { + setSetting(key, password); + save = true; + wsSend_P(client_id, PSTR("{\"action\": \"reload\"}")); + } + } else { + wsSend_P(client_id, PSTR("{\"message\": 7}")); + } + continue; + } + + // Store values + if (value.is()) { + if (_wsStore(key, value.as())) changed = true; + } else { + if (_wsStore(key, value.as())) changed = true; + } + + // Update flags if value has changed + if (changed) { + save = true; + #if MQTT_SUPPORT + if (key.startsWith("mqtt")) changedMQTT = true; + #endif + } + + } + + // Save settings + if (save) { + + // Callbacks + for (unsigned char i = 0; i < _ws_on_after_parse_callbacks.size(); i++) { + (_ws_on_after_parse_callbacks[i])(); + } + + // This should got to callback as well + // but first change management has to be in place + #if MQTT_SUPPORT + if (changedMQTT) mqttReset(); + #endif + + // Persist settings + saveSettings(); + + wsSend_P(client_id, PSTR("{\"message\": 8}")); + + } else { + + wsSend_P(client_id, PSTR("{\"message\": 9}")); + + } + + } + +} + +void _wsUpdate(JsonObject& root) { + root["heap"] = getFreeHeap(); + root["uptime"] = getUptime(); + root["rssi"] = WiFi.RSSI(); + root["distance"] = wifiDistance(WiFi.RSSI()); + #if NTP_SUPPORT + if (ntpSynced()) root["now"] = now(); + #endif +} + +void _wsOnStart(JsonObject& root) { + + #if USE_PASSWORD && WEB_FORCE_PASS_CHANGE + String adminPass = getSetting("adminPass", ADMIN_PASS); + bool changePassword = adminPass.equals(ADMIN_PASS); + #else + bool changePassword = false; + #endif + + if (changePassword) { + + root["webMode"] = WEB_MODE_PASSWORD; + + } else { + + char chipid[7]; + snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId()); + uint8_t * bssid = WiFi.BSSID(); + char bssid_str[20]; + snprintf_P(bssid_str, sizeof(bssid_str), + PSTR("%02X:%02X:%02X:%02X:%02X:%02X"), + bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5] + ); + + root["webMode"] = WEB_MODE_NORMAL; + + root["app_name"] = APP_NAME; + root["app_version"] = APP_VERSION; + root["app_build"] = buildTime(); + root["manufacturer"] = MANUFACTURER; + root["chipid"] = String(chipid); + root["mac"] = WiFi.macAddress(); + root["bssid"] = String(bssid_str); + root["channel"] = WiFi.channel(); + root["device"] = DEVICE; + root["hostname"] = getSetting("hostname"); + root["network"] = getNetwork(); + root["deviceip"] = getIP(); + root["sketch_size"] = ESP.getSketchSize(); + root["free_size"] = ESP.getFreeSketchSpace(); + + _wsUpdate(root); + + root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt(); + root["webPort"] = getSetting("webPort", WEB_PORT).toInt(); + root["tmpUnits"] = getSetting("tmpUnits", SENSOR_TEMPERATURE_UNITS).toInt(); + root["tmpCorrection"] = getSetting("tmpCorrection", SENSOR_TEMPERATURE_CORRECTION).toFloat(); + + } + +} + +void _wsStart(uint32_t client_id) { + for (unsigned char i = 0; i < _ws_on_send_callbacks.size(); i++) { + wsSend(client_id, _ws_on_send_callbacks[i]); + } +} + +void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + + if (type == WS_EVT_CONNECT) { + IPAddress ip = client->remoteIP(); + 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); + + } + +} + +void _wsLoop() { + static unsigned long last = 0; + if (!wsConnected()) return; + if (millis() - last > WS_UPDATE_INTERVAL) { + last = millis(); + wsSend(_wsUpdate); + } +} + +// ----------------------------------------------------------------------------- +// Piblic API +// ----------------------------------------------------------------------------- + +bool wsConnected() { + return (_ws.count() > 0); +} + +void wsOnSendRegister(ws_on_send_callback_f callback) { + _ws_on_send_callbacks.push_back(callback); +} + +void wsOnActionRegister(ws_on_action_callback_f callback) { + _ws_on_action_callbacks.push_back(callback); +} + +void wsOnAfterParseRegister(ws_on_after_parse_callback_f callback) { + _ws_on_after_parse_callbacks.push_back(callback); +} + +void wsSend(ws_on_send_callback_f callback) { + if (_ws.count() > 0) { + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + callback(root); + String output; + root.printTo(output); + _ws.textAll((char *) output.c_str()); + } +} + +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, ws_on_send_callback_f callback) { + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + callback(root); + String output; + root.printTo(output); + _ws.text(client_id, (char *) output.c_str()); +} + +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() { + #if USE_PASSWORD + _ws.setAuthentication(WEB_USERNAME, (const char *) getSetting("adminPass", ADMIN_PASS).c_str()); + #endif +} + +void wsSetup() { + _ws.onEvent(_wsEvent); + wsConfigure(); + webServer()->addHandler(&_ws); + #if MQTT_SUPPORT + mqttRegister(_wsMQTTCallback); + #endif + wsOnSendRegister(_wsOnStart); + wsOnAfterParseRegister(wsConfigure); + espurnaRegisterLoop(_wsLoop); +} + +#endif // WEB_SUPPORT diff --git a/code/html/custom.css b/code/html/custom.css index a9597d68..203ef68b 100644 --- a/code/html/custom.css +++ b/code/html/custom.css @@ -1,281 +1,281 @@ -/* ----------------------------------------------------------------------------- - General - -------------------------------------------------------------------------- */ - -#menu .pure-menu-heading { - font-size: 100%; - padding: .5em .5em; -} - -.pure-g { - margin-bottom: 0; -} - -.pure-form legend { - font-weight: bold; - letter-spacing: 0; - margin: 10px 0 1em 0; -} - -.pure-form .pure-g > label { - margin: .4em 0 .2em; -} - -.pure-form input { - margin-bottom: 10px; -} - -.pure-form input[type=text][disabled] { - color: #777777; -} - -h2 { - font-size: 1em; -} - -.panel { - display: none; -} - -.block { - display: block; -} - -.content { - margin: 0; -} - -.page { - margin-top: 10px; -} - -.hint { - color: #ccc; - font-size: 80%; - margin: -10px 0 10px 0; -} - -legend.module, -.module { - display: none; -} - -.template { - display: none; -} - -input[name=upgrade] { - display: none; -} - -select { - margin-bottom: 10px; - width: 100%; -} - -input.center { - margin-bottom: 0; -} - -div.center { - margin: .5em 0 1em; -} - -.webmode { - display: none; -} - -#credentials { - font-size: 200%; - height: 100px; - left: 50%; - margin-left: -200px; - margin-top: -50px; - position: fixed; - text-align: center; - top: 50%; - width: 400px; -} - -div.state { - border-top: 1px solid #eee; - margin-top: 20px; - padding-top: 30px; -} - -.state div { - font-size: 80%; -} - -.state span { - font-size: 80%; - font-weight: bold; -} - -.right { - text-align: right; -} - -/* ----------------------------------------------------------------------------- - Buttons - -------------------------------------------------------------------------- */ - -.pure-button { - border-radius: 4px; - color: white; - letter-spacing: 0; - margin-bottom: 10px; - padding: 8px 8px; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -} - -.main-buttons { - margin: 20px auto; - text-align: center; -} -.main-buttons button { - width: 100px; -} - -.button-reboot, -.button-reconnect, -.button-ha-del, -.button-rfb-forget, -.button-del-network, -.button-del-schedule, -.button-upgrade, -.button-settings-factory { - background: rgb(192, 0, 0); /* redish */ -} - -.button-update, -.button-update-password, -.button-add-network, -.button-add-schedule, -.button-rfb-learn, -.button-upgrade-browse, -.button-ha-add, -.button-settings-backup, -.button-settings-restore, -.button-apikey { - background: rgb(0, 192, 0); /* green */ -} - -.button-more-network, -.button-more-schedule, -.button-wifi-scan, -.button-rfb-send { - background: rgb(255, 128, 0); /* orange */ -} - -.button-upgrade-browse, -.button-ha-add, -.button-apikey, -.button-upgrade { - margin-left: 5px; -} - -/* ----------------------------------------------------------------------------- - Sliders - -------------------------------------------------------------------------- */ - -input.slider { - margin-top: 10px; -} - -span.slider { - font-size: 70%; - letter-spacing: 0; - margin-left: 10px; - margin-top: 7px; -} - -/* ----------------------------------------------------------------------------- - Loading - -------------------------------------------------------------------------- */ - -.loading { - background-image: url('images/loading.gif'); - display: none; - height: 20px; - margin: 8px 0 0 10px; - width: 20px; -} - -/* ----------------------------------------------------------------------------- - Menu - -------------------------------------------------------------------------- */ - -#menu .small { - font-size: 60%; - padding-left: 9px; -} - -#menu div.footer { - color: #999; - font-size: 80%; - padding: 10px; -} -#menu div.footer a { - padding: 0; - text-decoration: none; -} - -/* ----------------------------------------------------------------------------- - RF Bridge panel - -------------------------------------------------------------------------- */ - -#panel-rfb fieldset { - margin: 10px 2px; - padding: 20px; -} - -#panel-rfb input { - margin-right: 5px; -} - -#panel-rfb label { - padding-top: 5px; -} - -#panel-rfb input { - text-align: center; -} - -/* ----------------------------------------------------------------------------- - Admin panel - -------------------------------------------------------------------------- */ - -#upgrade-progress { - display: none; - height: 20px; - margin-top: 10px; - width: 100%; -} - -#uploader, -#downloader { - display: none; -} - -/* ----------------------------------------------------------------------------- - Wifi panel - -------------------------------------------------------------------------- */ - -#networks .pure-g, -#schedules .pure-g { - border-bottom: 1px solid #eee; - margin-bottom: 10px; - padding: 10px 0 10px 0; -} - -#networks .more { - display: none; -} - -#scanResult { - color: #888; - font-family: 'Courier New', monospace; - font-size: 60%; - margin-top: 10px; -} +/* ----------------------------------------------------------------------------- + General + -------------------------------------------------------------------------- */ + +#menu .pure-menu-heading { + font-size: 100%; + padding: .5em .5em; +} + +.pure-g { + margin-bottom: 0; +} + +.pure-form legend { + font-weight: bold; + letter-spacing: 0; + margin: 10px 0 1em 0; +} + +.pure-form .pure-g > label { + margin: .4em 0 .2em; +} + +.pure-form input { + margin-bottom: 10px; +} + +.pure-form input[type=text][disabled] { + color: #777777; +} + +h2 { + font-size: 1em; +} + +.panel { + display: none; +} + +.block { + display: block; +} + +.content { + margin: 0; +} + +.page { + margin-top: 10px; +} + +.hint { + color: #ccc; + font-size: 80%; + margin: -10px 0 10px 0; +} + +legend.module, +.module { + display: none; +} + +.template { + display: none; +} + +input[name=upgrade] { + display: none; +} + +select { + margin-bottom: 10px; + width: 100%; +} + +input.center { + margin-bottom: 0; +} + +div.center { + margin: .5em 0 1em; +} + +.webmode { + display: none; +} + +#credentials { + font-size: 200%; + height: 100px; + left: 50%; + margin-left: -200px; + margin-top: -50px; + position: fixed; + text-align: center; + top: 50%; + width: 400px; +} + +div.state { + border-top: 1px solid #eee; + margin-top: 20px; + padding-top: 30px; +} + +.state div { + font-size: 80%; +} + +.state span { + font-size: 80%; + font-weight: bold; +} + +.right { + text-align: right; +} + +/* ----------------------------------------------------------------------------- + Buttons + -------------------------------------------------------------------------- */ + +.pure-button { + border-radius: 4px; + color: white; + letter-spacing: 0; + margin-bottom: 10px; + padding: 8px 8px; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); +} + +.main-buttons { + margin: 20px auto; + text-align: center; +} +.main-buttons button { + width: 100px; +} + +.button-reboot, +.button-reconnect, +.button-ha-del, +.button-rfb-forget, +.button-del-network, +.button-del-schedule, +.button-upgrade, +.button-settings-factory { + background: rgb(192, 0, 0); /* redish */ +} + +.button-update, +.button-update-password, +.button-add-network, +.button-add-schedule, +.button-rfb-learn, +.button-upgrade-browse, +.button-ha-add, +.button-settings-backup, +.button-settings-restore, +.button-apikey { + background: rgb(0, 192, 0); /* green */ +} + +.button-more-network, +.button-more-schedule, +.button-wifi-scan, +.button-rfb-send { + background: rgb(255, 128, 0); /* orange */ +} + +.button-upgrade-browse, +.button-ha-add, +.button-apikey, +.button-upgrade { + margin-left: 5px; +} + +/* ----------------------------------------------------------------------------- + Sliders + -------------------------------------------------------------------------- */ + +input.slider { + margin-top: 10px; +} + +span.slider { + font-size: 70%; + letter-spacing: 0; + margin-left: 10px; + margin-top: 7px; +} + +/* ----------------------------------------------------------------------------- + Loading + -------------------------------------------------------------------------- */ + +.loading { + background-image: url('images/loading.gif'); + display: none; + height: 20px; + margin: 8px 0 0 10px; + width: 20px; +} + +/* ----------------------------------------------------------------------------- + Menu + -------------------------------------------------------------------------- */ + +#menu .small { + font-size: 60%; + padding-left: 9px; +} + +#menu div.footer { + color: #999; + font-size: 80%; + padding: 10px; +} +#menu div.footer a { + padding: 0; + text-decoration: none; +} + +/* ----------------------------------------------------------------------------- + RF Bridge panel + -------------------------------------------------------------------------- */ + +#panel-rfb fieldset { + margin: 10px 2px; + padding: 20px; +} + +#panel-rfb input { + margin-right: 5px; +} + +#panel-rfb label { + padding-top: 5px; +} + +#panel-rfb input { + text-align: center; +} + +/* ----------------------------------------------------------------------------- + Admin panel + -------------------------------------------------------------------------- */ + +#upgrade-progress { + display: none; + height: 20px; + margin-top: 10px; + width: 100%; +} + +#uploader, +#downloader { + display: none; +} + +/* ----------------------------------------------------------------------------- + Wifi panel + -------------------------------------------------------------------------- */ + +#networks .pure-g, +#schedules .pure-g { + border-bottom: 1px solid #eee; + margin-bottom: 10px; + padding: 10px 0 10px 0; +} + +#networks .more { + display: none; +} + +#scanResult { + color: #888; + font-family: 'Courier New', monospace; + font-size: 60%; + margin-top: 10px; +} diff --git a/code/html/custom.js b/code/html/custom.js index 81da6689..22a41853 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -1,1280 +1,1280 @@ -var websock; -var password = false; -var maxNetworks; -var maxSchedules; -var messages = []; -var webhost; - -var numChanged = 0; -var numReboot = 0; -var numReconnect = 0; -var numReload = 0; - -var useWhite = false; -var manifest; - -var now = 0; -var ago = 0; - -// ----------------------------------------------------------------------------- -// Messages -// ----------------------------------------------------------------------------- - -function initMessages() { - messages[1] = "Remote update started"; - messages[2] = "OTA update started"; - messages[3] = "Error parsing data!"; - messages[4] = "The file does not look like a valid configuration backup or is corrupted"; - messages[5] = "Changes saved. You should reboot your board now"; - messages[7] = "Passwords do not match!"; - messages[8] = "Changes saved"; - messages[9] = "No changes detected"; - messages[10] = "Session expired, please reload page..."; -} - -function sensorName(id) { - var names = [ - "DHT", "Dallas", "Emon Analog", "Emon ADC121", "Emon ADS1X15", - "HLW8012", "V9261F", "ECH1560", "Analog", "Digital", - "Events", "PMSX003", "BMX280", "MHZ19", "SI7021", - "SHT3X I2C", "BH1750" - ]; - if (1 <= id && id <= names.length) { - return names[id - 1]; - } - return null; -} - -function magnitudeType(type) { - var types = [ - "Temperature", "Humidity", "Pressure", - "Current", "Voltage", "Active Power", "Apparent Power", - "Reactive Power", "Power Factor", "Energy", "Energy (delta)", - "Analog", "Digital", "Events", - "PM1.0", "PM2.5", "PM10", "CO2", "Lux" - ]; - if (1 <= type && type <= types.length) { - return types[type - 1]; - } - return null; -} - -function magnitudeError(error) { - var errors = [ - "OK", "Out of Range", "Warming Up", "Timeout", "Wrong ID", - "Data Error", "I2C Error", "GPIO Error" - ]; - if (0 <= error && error < errors.length) { - return errors[error]; - } - return "Error " + error; -} - -// ----------------------------------------------------------------------------- -// Utils -// ----------------------------------------------------------------------------- - -function keepTime() { - if (now === 0) { return; } - var date = new Date(now * 1000); - var text = date.toISOString().substring(0, 19).replace("T", " "); - $("input[name='now']").val(text); - $("span[name='now']").html(text); - $("span[name='ago']").html(ago); - now++; - ago++; -} - -// http://www.the-art-of-web.com/javascript/validate-password/ -function checkPassword(str) { - // at least one lowercase and one uppercase letter or number - // at least five characters (letters, numbers or special characters) - var re = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*\(\)<>,.\?;:{}\[\]\\|]{5,}$/; - return re.test(str); -} - -function zeroPad(number, positions) { - var zeros = ""; - for (var i = 0; i < positions; i++) { - zeros += "0"; - } - return (zeros + number).slice(-positions); -} - -function loadTimeZones() { - - var time_zones = [ - -720, -660, -600, -570, -540, - -480, -420, -360, -300, -240, - -210, -180, -120, -60, 0, - 60, 120, 180, 210, 240, - 270, 300, 330, 345, 360, - 390, 420, 480, 510, 525, - 540, 570, 600, 630, 660, - 720, 765, 780, 840 - ]; - - for (var i in time_zones) { - var value = parseInt(time_zones[i], 10); - var offset = value >= 0 ? value : -value; - var text = "GMT" + (value >= 0 ? "+" : "-") + - zeroPad(parseInt(offset / 60, 10), 2) + ":" + - zeroPad(offset % 60, 2); - $("select[name='ntpOffset']").append( - $(""). - attr("value",value). - text(text)); - } - -} - -function validateForm(form) { - - // password - var adminPass1 = $("input[name='adminPass']", form).first().val(); - if (adminPass1.length > 0 && !checkPassword(adminPass1)) { - alert("The password you have entered is not valid, it must have at least 5 characters, 1 lowercase and 1 uppercase or number!"); - return false; - } - - var adminPass2 = $("input[name='adminPass']", form).last().val(); - if (adminPass1 !== adminPass2) { - alert("Passwords are different!"); - return false; - } - - return true; - -} - -// These fields will always be a list of values -var is_group = [ - "ssid", "pass", "gw", "mask", "ip", "dns", - "schEnabled", "schSwitch","schAction","schHour","schMinute","schWDs", - "relayBoot", "relayPulse", "relayTime", - "mqttGroup", "mqttGroupInv", - "dczRelayIdx", "dczMagnitude", - "tspkRelay", "tspkMagnitude", - "ledMode", - "adminPass" -]; - -function getData(form) { - - var data = {}; - - // Populate data - $("input,select", form).each(function() { - var name = $(this).attr("name"); - if (name) { - var value = ""; - - // Do not report these fields - if (name === "filename" || name === "rfbcode" ) { - return; - } - - // Grab the value - if ($(this).attr("type") === "checkbox") { - value = $(this).is(":checked") ? 1 : 0; - } else if ($(this).attr("type") === "radio") { - if (!$(this).is(":checked")) {return;} - value = $(this).val(); - } else { - value = $(this).val(); - } - - // Build the object - if (name in data) { - if (!Array.isArray(data[name])) data[name] = [data[name]]; - data[name].push(value); - } else if (is_group.indexOf(name) >= 0) { - data[name] = [value]; - } else { - data[name] = value; - } - - } - }); - - // Post process - if ("schSwitch" in data) { - data["schSwitch"].push(0xFF); - } else { - data["schSwitch"] = [0xFF]; - } - - return data; - -} - -function randomString(length, chars) { - var mask = ""; - if (chars.indexOf("a") > -1) { mask += "abcdefghijklmnopqrstuvwxyz"; } - if (chars.indexOf("A") > -1) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; } - if (chars.indexOf("#") > -1) { mask += "0123456789"; } - if (chars.indexOf("@") > -1) { mask += "ABCDEF"; } - if (chars.indexOf("!") > -1) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; } - var result = ""; - for (var i = length; i > 0; --i) { - result += mask[Math.round(Math.random() * (mask.length - 1))]; - } - return result; -} - -function generateAPIKey() { - var apikey = randomString(16, "@#"); - $("input[name='apiKey']").val(apikey); - return false; -} - -function getJson(str) { - try { - return JSON.parse(str); - } catch (e) { - return false; - } -} - -// ----------------------------------------------------------------------------- -// Actions -// ----------------------------------------------------------------------------- - -function resetOriginals() { - $("input,select").each(function() { - $(this).attr("original", $(this).val()); - }); - numReboot = numReconnect = numReload = 0; -} - -function doReload(milliseconds) { - milliseconds = (typeof milliseconds == "undefined") ? - 0 : - parseInt(milliseconds, 10); - setTimeout(function() { - window.location.reload(); - }, milliseconds); -} - -function doUpgrade() { - - var contents = $("input[name='upgrade']")[0].files[0]; - if (typeof contents === "undefined") { - alert("First you have to select a file from your computer."); - return false; - } - var filename = $("input[name='upgrade']").val().split("\\").pop(); - - var data = new FormData(); - data.append("upgrade", contents, filename); - - $.ajax({ - - // Your server script to process the upload - url: webhost + "upgrade", - type: "POST", - - // Form data - data: data, - - // Tell jQuery not to process data or worry about content-type - // You *must* include these options! - cache: false, - contentType: false, - processData: false, - - success: function(data, text) { - $("#upgrade-progress").hide(); - if (data === "OK") { - alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds."); - doReload(5000); - } else { - alert("There was an error trying to upload the new image, please try again (" + data + ")."); - } - }, - - // Custom XMLHttpRequest - xhr: function() { - $("#upgrade-progress").show(); - var myXhr = $.ajaxSettings.xhr(); - if (myXhr.upload) { - // For handling the progress of the upload - myXhr.upload.addEventListener("progress", function(e) { - if (e.lengthComputable) { - $("progress").attr({ value: e.loaded, max: e.total }); - } - } , false); - } - return myXhr; - } - - }); - - return false; - -} - -function doUpdatePassword() { - var form = $("#formPassword"); - if (validateForm(form)) { - var data = getData(form); - websock.send(JSON.stringify({"config": data})); - } - return false; -} - -function doReboot(ask) { - - var response; - - ask = (typeof ask == "undefined") ? true : ask; - - if (numChanged > 0) { - response = window.confirm("Some changes have not been saved yet, do you want to save them first?"); - if (response === true) { - return doUpdate(); - } - } - - if (ask) { - response = window.confirm("Are you sure you want to reboot the device?"); - if (response === false) { - return false; - } - } - - websock.send(JSON.stringify({"action": "reboot"})); - doReload(5000); - return false; - -} - -function doReconnect(ask) { - var response; - - ask = (typeof ask == "undefined") ? true : ask; - - if (numChanged > 0) { - response = window.confirm("Some changes have not been saved yet, do you want to save them first?"); - if (response === true) { - return doUpdate(); - } - } - - if (ask) { - response = window.confirm("Are you sure you want to disconnect from the current WIFI network?"); - if (response === false) { - return false; - } - } - - websock.send(JSON.stringify({"action": "reconnect"})); - doReload(5000); - return false; - -} - -function doUpdate() { - - var form = $("#formSave"); - if (validateForm(form)) { - - // Get data - var data = getData(form); - websock.send(JSON.stringify({"config": data})); - - // Empty special fields - $(".pwrExpected").val(0); - $("input[name='pwrResetCalibration']"). - prop("checked", false). - iphoneStyle("refresh"); - - // Change handling - numChanged = 0; - setTimeout(function() { - - var response; - - if (numReboot > 0) { - response = window.confirm("You have to reboot the board for the changes to take effect, do you want to do it now?"); - if (response === true) { doReboot(false); } - } else if (numReconnect > 0) { - response = window.confirm("You have to reconnect to the WiFi for the changes to take effect, do you want to do it now?"); - if (response === true) { doReconnect(false); } - } else if (numReload > 0) { - response = window.confirm("You have to reload the page to see the latest changes, do you want to do it now?"); - if (response === true) { doReload(); } - } - - resetOriginals(); - - }, 1000); - - } - - return false; - -} - -function doBackup() { - document.getElementById("downloader").src = webhost + "config"; - return false; -} - -function onFileUpload(event) { - - var inputFiles = this.files; - if (inputFiles === undefined || inputFiles.length === 0) { - return false; - } - var inputFile = inputFiles[0]; - this.value = ""; - - var response = window.confirm("Previous settings will be overwritten. Are you sure you want to restore this settings?"); - if (response === false) { - return false; - } - - var reader = new FileReader(); - reader.onload = function(e) { - var data = getJson(e.target.result); - if (data) { - websock.send(JSON.stringify({"action": "restore", "data": data})); - } else { - alert(messages[4]); - } - }; - reader.readAsText(inputFile); - - return false; - -} - -function doRestore() { - if (typeof window.FileReader !== "function") { - alert("The file API isn't supported on this browser yet."); - } else { - $("#uploader").click(); - } - return false; -} - -function doFactoryReset() { - var response; - - ask = (typeof ask == "undefined") ? true : ask; - - if (numChanged > 0) { - response = window.confirm("Some changes have not been saved yet, do you want to save them first?"); - if (response === true) { - return doUpdate(); - } - } - - if (ask) { - response = window.confirm("Are you sure you want to restore to factory settings?"); - if (response === false) { - return false; - } - } - - websock.send(JSON.stringify({"action": "factory_reset"})); - doReload(5000); - return false; -} - -function doToggle(element, value) { - var relayID = parseInt(element.attr("data"), 10); - websock.send(JSON.stringify({"action": "relay", "data": { "id": relayID, "status": value ? 1 : 0 }})); - return false; -} - -function doScan() { - $("#scanResult").html(""); - $("div.scan.loading").show(); - websock.send(JSON.stringify({"action": "scan", "data": {}})); - return false; -} - -// ----------------------------------------------------------------------------- -// Visualization -// ----------------------------------------------------------------------------- - -function toggleMenu() { - $("#layout").toggleClass("active"); - $("#menu").toggleClass("active"); - $("#menuLink").toggleClass("active"); -} - -function showPanel() { - $(".panel").hide(); - $("#" + $(this).attr("data")).show(); - if ($("#layout").hasClass("active")) { toggleMenu(); } - $("input[type='checkbox']"). - iphoneStyle("calculateDimensions"). - iphoneStyle("refresh"); -} - -// ----------------------------------------------------------------------------- -// Relays & magnitudes mapping -// ----------------------------------------------------------------------------- - -function createRelayList(data, container, template_name) { - - var current = $("#" + container + " > div").length; - if (current > 0) { - return; - } - - var template = $("#" + template_name + " .pure-g")[0]; - for (var i=0; i div").length; - if (current > 0) { - return; - } - - var template = $("#" + template_name + " .pure-g")[0]; - for (var i=0; i div").length; - if (numNetworks >= maxNetworks) { - alert("Max number of networks reached"); - return null; - } - - var tabindex = 200 + numNetworks * 10; - var template = $("#networkTemplate").children(); - var line = $(template).clone(); - $(line).find("input").each(function() { - $(this).attr("tabindex", tabindex); - tabindex++; - }); - $(line).find(".button-del-network").on("click", delNetwork); - $(line).find(".button-more-network").on("click", moreNetwork); - line.appendTo("#networks"); - - return line; - -} - -// ----------------------------------------------------------------------------- -// Relays scheduler -// ----------------------------------------------------------------------------- -function delSchedule() { - var parent = $(this).parents(".pure-g"); - $(parent).remove(); -} - -function moreSchedule() { - var parent = $(this).parents(".pure-g"); - $("div.more", parent).toggle(); -} - -function addSchedule() { - var numSchedules = $("#schedules > div").length; - if (numSchedules >= maxSchedules) { - alert("Max number of schedules reached"); - return null; - } - var tabindex = 200 + numSchedules * 10; - var template = $("#scheduleTemplate").children(); - var line = $(template).clone(); - $(line).find("input").each(function() { - $(this).attr("tabindex", tabindex); - tabindex++; - }); - $(line).find(".button-del-schedule").on("click", delSchedule); - $(line).find(".button-more-schedule").on("click", moreSchedule); - line.appendTo("#schedules"); - return line; -} - -// ----------------------------------------------------------------------------- -// Relays -// ----------------------------------------------------------------------------- - -function initRelays(data) { - - var current = $("#relays > div").length; - if (current > 0) { - return; - } - - var template = $("#relayTemplate .pure-g")[0]; - for (var i=0; i").attr("value",i).text("Switch #" + i)); - - } - - -} - -function initRelayConfig(data) { - - var current = $("#relayConfig > div").length; - if (current > 0) { - return; - } - - var template = $("#relayConfigTemplate").children(); - for (var i=0; i < data.length; i++) { - var line = $(template).clone(); - $("span.gpio", line).html(data[i].gpio); - $("span.id", line).html(i); - $("select[name='relayBoot']", line).val(data[i].boot); - $("select[name='relayPulse']", line).val(data[i].pulse); - $("input[name='relayTime']", line).val(data[i].pulse_ms); - $("input[name='mqttGroup']", line).val(data[i].group); - $("select[name='mqttGroupInv']", line).val(data[i].group_inv); - line.appendTo("#relayConfig"); - } - -} - -// ----------------------------------------------------------------------------- -// Sensors & Magnitudes -// ----------------------------------------------------------------------------- - -function initMagnitudes(data) { - - // check if already initialized - var done = $("#magnitudes > div").length; - if (done > 0) { - return; - } - - // add templates - var template = $("#magnitudeTemplate").children(); - for (var i=0; i div").length; - if (done > 0) { - return; - } - - // add template - var template = $("#colorRGBTemplate").children(); - var line = $(template).clone(); - line.appendTo("#colors"); - - // init color wheel - $("input[name='color']").wheelColorPicker({ - sliders: "wrgbp" - }).on("sliderup", function() { - var value = $(this).wheelColorPicker("getValue", "css"); - websock.send(JSON.stringify({"action": "color", "data" : {"rgb": value}})); - }); - - // init bright slider - $("#brightness").on("change", function() { - var value = $(this).val(); - var parent = $(this).parents(".pure-g"); - $("span", parent).html(value); - websock.send(JSON.stringify({"action": "color", "data" : {"brightness": value}})); - }); - -} - -function initColorHSV() { - - // check if already initialized - var done = $("#colors > div").length; - if (done > 0) { - return; - } - - // add template - var template = $("#colorHSVTemplate").children(); - var line = $(template).clone(); - line.appendTo("#colors"); - - // init color wheel - $("input[name='color']").wheelColorPicker({ - sliders: "whsvp" - }).on("sliderup", function() { - var color = $(this).wheelColorPicker("getColor"); - var value = parseInt(color.h * 360, 10) + "," + parseInt(color.s * 100, 10) + "," + parseInt(color.v * 100, 10); - websock.send(JSON.stringify({"action": "color", "data" : {"hsv": value}})); - }); - -} - -function initChannels(num) { - - // check if already initialized - var done = $("#channels > div").length > 0; - if (done) { - return; - } - - // does it have color channels? - var colors = $("#colors > div").length > 0; - - // calculate channels to create - var max = num; - if (colors) { - max = num % 3; - if ((max > 0) & useWhite) max--; - } - var start = num - max; - - // add templates - var template = $("#channelTemplate").children(); - for (var i=0; i legend").length; - - var template = $("#rfbNodeTemplate").children(); - var line = $(template).clone(); - var status = true; - $("span", line).html(numNodes); - $(line).find("input").each(function() { - $(this).attr("data-id", numNodes); - $(this).attr("data-status", status ? 1 : 0); - status = !status; - }); - $(line).find(".button-rfb-learn").on("click", rfbLearn); - $(line).find(".button-rfb-forget").on("click", rfbForget); - $(line).find(".button-rfb-send").on("click", rfbSend); - line.appendTo("#rfbNodes"); - - return line; -} - -function rfbLearn() { - var parent = $(this).parents(".pure-g"); - var input = $("input", parent); - websock.send(JSON.stringify({"action": "rfblearn", "data" : {"id" : input.attr("data-id"), "status": input.attr("data-status")}})); -} - -function rfbForget() { - var parent = $(this).parents(".pure-g"); - var input = $("input", parent); - websock.send(JSON.stringify({"action": "rfbforget", "data" : {"id" : input.attr("data-id"), "status": input.attr("data-status")}})); -} - -function rfbSend() { - var parent = $(this).parents(".pure-g"); - var input = $("input", parent); - websock.send(JSON.stringify({"action": "rfbsend", "data" : {"id" : input.attr("data-id"), "status": input.attr("data-status"), "data": input.val()}})); -} - -// ----------------------------------------------------------------------------- -// Processing -// ----------------------------------------------------------------------------- - -function processData(data) { - - // title - if ("app_name" in data) { - var title = data.app_name; - if ("app_version" in data) { - title = title + " " + data.app_version; - } - $("span[name=title]").html(title); - if ("hostname" in data) { - title = data.hostname + " - " + title; - } - document.title = title; - } - - Object.keys(data).forEach(function(key) { - - var i; - - // --------------------------------------------------------------------- - // Web mode - // --------------------------------------------------------------------- - - if (key ==="webMode") { - password = data.webMode == 1; - $("#layout").toggle(data.webMode === 0); - $("#password").toggle(data.webMode === 1); - } - - // --------------------------------------------------------------------- - // Actions - // --------------------------------------------------------------------- - - if (key === "action") { - if (data.action === "reload") doReload(1000); - return; - } - - // --------------------------------------------------------------------- - // RFBridge - // --------------------------------------------------------------------- - - if (key === "rfbCount") { - for (i=0; i 0 && position === key.length - 7) { - var module = key.slice(0,-7); - $(".module-" + module).show(); - return; - } - - if (key === "now") { - now = data[key]; - ago = 0; - return; - } - - // Pre-process - if (key === "network") { - data.network = data.network.toUpperCase(); - } - if (key === "mqttStatus") { - data.mqttStatus = data.mqttStatus ? "CONNECTED" : "NOT CONNECTED"; - } - if (key === "ntpStatus") { - data.ntpStatus = data.ntpStatus ? "SYNC'D" : "NOT SYNC'D"; - } - if (key === "uptime") { - var uptime = parseInt(data[key], 10); - var seconds = uptime % 60; uptime = parseInt(uptime / 60, 10); - var minutes = uptime % 60; uptime = parseInt(uptime / 60, 10); - var hours = uptime % 24; uptime = parseInt(uptime / 24, 10); - var days = uptime; - data[key] = days + "d " + zeroPad(hours, 2) + "h " + zeroPad(minutes, 2) + "m " + zeroPad(seconds, 2) + "s"; - } - - // --------------------------------------------------------------------- - // Matching - // --------------------------------------------------------------------- - - var pre; - var post; - - // Look for INPUTs - var input = $("input[name='" + key + "']"); - if (input.length > 0) { - if (input.attr("type") === "checkbox") { - input. - prop("checked", data[key]). - iphoneStyle("refresh"); - } else if (input.attr("type") === "radio") { - input.val([data[key]]); - } else { - pre = input.attr("pre") || ""; - post = input.attr("post") || ""; - input.val(pre + data[key] + post); - } - } - - // Look for SPANs - var span = $("span[name='" + key + "']"); - if (span.length > 0) { - pre = span.attr("pre") || ""; - post = span.attr("post") || ""; - span.html(pre + data[key] + post); - } - - // Look for SELECTs - var select = $("select[name='" + key + "']"); - if (select.length > 0) { - select.val(data[key]); - } - - }); - - // Auto generate an APIKey if none defined yet - if ($("input[name='apiKey']").val() === "") { - generateAPIKey(); - } - - resetOriginals(); - -} - -function hasChanged() { - - var newValue, originalValue; - if ($(this).attr("type") === "checkbox") { - newValue = $(this).prop("checked"); - originalValue = $(this).attr("original") == "true"; - } else { - newValue = $(this).val(); - originalValue = $(this).attr("original"); - } - var hasChanged = $(this).attr("hasChanged") || 0; - var action = $(this).attr("action"); - - if (typeof originalValue === "undefined") { return; } - if (action === "none") { return; } - - if (newValue !== originalValue) { - if (hasChanged === 0) { - ++numChanged; - if (action === "reconnect") ++numReconnect; - if (action === "reboot") ++numReboot; - if (action === "reload") ++numReload; - $(this).attr("hasChanged", 1); - } - } else { - if (hasChanged === 1) { - --numChanged; - if (action === "reconnect") --numReconnect; - if (action === "reboot") --numReboot; - if (action === "reload") --numReload; - $(this).attr("hasChanged", 0); - } - } - -} - -// ----------------------------------------------------------------------------- -// Init & connect -// ----------------------------------------------------------------------------- - -function connect(host) { - - if (typeof host === "undefined") { - host = window.location.href.replace("#", ""); - } else { - if (host.indexOf("http") !== 0) { - host = "http://" + host + "/"; - } - } - if (host.indexOf("http") !== 0) {return;} - - webhost = host; - wshost = host.replace("http", "ws") + "ws"; - - if (websock) websock.close(); - websock = new WebSocket(wshost); - websock.onmessage = function(evt) { - var data = getJson(evt.data); - if (data) processData(data); - }; -} - -$(function() { - - initMessages(); - loadTimeZones(); - setInterval(function() { keepTime(); }, 1000); - - $("#menuLink").on("click", toggleMenu); - $(".pure-menu-link").on("click", showPanel); - $("progress").attr({ value: 0, max: 100 }); - - $(".button-update").on("click", doUpdate); - $(".button-update-password").on("click", doUpdatePassword); - $(".button-reboot").on("click", doReboot); - $(".button-reconnect").on("click", doReconnect); - $(".button-wifi-scan").on("click", doScan); - $(".button-settings-backup").on("click", doBackup); - $(".button-settings-restore").on("click", doRestore); - $(".button-settings-factory").on("click", doFactoryReset); - $("#uploader").on("change", onFileUpload); - $(".button-upgrade").on("click", doUpgrade); - - $(".button-apikey").on("click", generateAPIKey); - $(".button-upgrade-browse").on("click", function() { - $("input[name='upgrade']")[0].click(); - return false; - }); - $("input[name='upgrade']").change(function (){ - var fileName = $(this).val(); - $("input[name='filename']").val(fileName.replace(/^.*[\\\/]/, "")); - }); - $(".button-add-network").on("click", function() { - $(".more", addNetwork()).toggle(); - }); - $(".button-add-schedule").on("click", addSchedule); - - $(document).on("change", "input", hasChanged); - $(document).on("change", "select", hasChanged); - - connect(); - -}); +var websock; +var password = false; +var maxNetworks; +var maxSchedules; +var messages = []; +var webhost; + +var numChanged = 0; +var numReboot = 0; +var numReconnect = 0; +var numReload = 0; + +var useWhite = false; +var manifest; + +var now = 0; +var ago = 0; + +// ----------------------------------------------------------------------------- +// Messages +// ----------------------------------------------------------------------------- + +function initMessages() { + messages[1] = "Remote update started"; + messages[2] = "OTA update started"; + messages[3] = "Error parsing data!"; + messages[4] = "The file does not look like a valid configuration backup or is corrupted"; + messages[5] = "Changes saved. You should reboot your board now"; + messages[7] = "Passwords do not match!"; + messages[8] = "Changes saved"; + messages[9] = "No changes detected"; + messages[10] = "Session expired, please reload page..."; +} + +function sensorName(id) { + var names = [ + "DHT", "Dallas", "Emon Analog", "Emon ADC121", "Emon ADS1X15", + "HLW8012", "V9261F", "ECH1560", "Analog", "Digital", + "Events", "PMSX003", "BMX280", "MHZ19", "SI7021", + "SHT3X I2C", "BH1750" + ]; + if (1 <= id && id <= names.length) { + return names[id - 1]; + } + return null; +} + +function magnitudeType(type) { + var types = [ + "Temperature", "Humidity", "Pressure", + "Current", "Voltage", "Active Power", "Apparent Power", + "Reactive Power", "Power Factor", "Energy", "Energy (delta)", + "Analog", "Digital", "Events", + "PM1.0", "PM2.5", "PM10", "CO2", "Lux" + ]; + if (1 <= type && type <= types.length) { + return types[type - 1]; + } + return null; +} + +function magnitudeError(error) { + var errors = [ + "OK", "Out of Range", "Warming Up", "Timeout", "Wrong ID", + "Data Error", "I2C Error", "GPIO Error" + ]; + if (0 <= error && error < errors.length) { + return errors[error]; + } + return "Error " + error; +} + +// ----------------------------------------------------------------------------- +// Utils +// ----------------------------------------------------------------------------- + +function keepTime() { + if (now === 0) { return; } + var date = new Date(now * 1000); + var text = date.toISOString().substring(0, 19).replace("T", " "); + $("input[name='now']").val(text); + $("span[name='now']").html(text); + $("span[name='ago']").html(ago); + now++; + ago++; +} + +// http://www.the-art-of-web.com/javascript/validate-password/ +function checkPassword(str) { + // at least one lowercase and one uppercase letter or number + // at least five characters (letters, numbers or special characters) + var re = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*\(\)<>,.\?;:{}\[\]\\|]{5,}$/; + return re.test(str); +} + +function zeroPad(number, positions) { + var zeros = ""; + for (var i = 0; i < positions; i++) { + zeros += "0"; + } + return (zeros + number).slice(-positions); +} + +function loadTimeZones() { + + var time_zones = [ + -720, -660, -600, -570, -540, + -480, -420, -360, -300, -240, + -210, -180, -120, -60, 0, + 60, 120, 180, 210, 240, + 270, 300, 330, 345, 360, + 390, 420, 480, 510, 525, + 540, 570, 600, 630, 660, + 720, 765, 780, 840 + ]; + + for (var i in time_zones) { + var value = parseInt(time_zones[i], 10); + var offset = value >= 0 ? value : -value; + var text = "GMT" + (value >= 0 ? "+" : "-") + + zeroPad(parseInt(offset / 60, 10), 2) + ":" + + zeroPad(offset % 60, 2); + $("select[name='ntpOffset']").append( + $(""). + attr("value",value). + text(text)); + } + +} + +function validateForm(form) { + + // password + var adminPass1 = $("input[name='adminPass']", form).first().val(); + if (adminPass1.length > 0 && !checkPassword(adminPass1)) { + alert("The password you have entered is not valid, it must have at least 5 characters, 1 lowercase and 1 uppercase or number!"); + return false; + } + + var adminPass2 = $("input[name='adminPass']", form).last().val(); + if (adminPass1 !== adminPass2) { + alert("Passwords are different!"); + return false; + } + + return true; + +} + +// These fields will always be a list of values +var is_group = [ + "ssid", "pass", "gw", "mask", "ip", "dns", + "schEnabled", "schSwitch","schAction","schHour","schMinute","schWDs", + "relayBoot", "relayPulse", "relayTime", + "mqttGroup", "mqttGroupInv", + "dczRelayIdx", "dczMagnitude", + "tspkRelay", "tspkMagnitude", + "ledMode", + "adminPass" +]; + +function getData(form) { + + var data = {}; + + // Populate data + $("input,select", form).each(function() { + var name = $(this).attr("name"); + if (name) { + var value = ""; + + // Do not report these fields + if (name === "filename" || name === "rfbcode" ) { + return; + } + + // Grab the value + if ($(this).attr("type") === "checkbox") { + value = $(this).is(":checked") ? 1 : 0; + } else if ($(this).attr("type") === "radio") { + if (!$(this).is(":checked")) {return;} + value = $(this).val(); + } else { + value = $(this).val(); + } + + // Build the object + if (name in data) { + if (!Array.isArray(data[name])) data[name] = [data[name]]; + data[name].push(value); + } else if (is_group.indexOf(name) >= 0) { + data[name] = [value]; + } else { + data[name] = value; + } + + } + }); + + // Post process + if ("schSwitch" in data) { + data["schSwitch"].push(0xFF); + } else { + data["schSwitch"] = [0xFF]; + } + + return data; + +} + +function randomString(length, chars) { + var mask = ""; + if (chars.indexOf("a") > -1) { mask += "abcdefghijklmnopqrstuvwxyz"; } + if (chars.indexOf("A") > -1) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; } + if (chars.indexOf("#") > -1) { mask += "0123456789"; } + if (chars.indexOf("@") > -1) { mask += "ABCDEF"; } + if (chars.indexOf("!") > -1) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; } + var result = ""; + for (var i = length; i > 0; --i) { + result += mask[Math.round(Math.random() * (mask.length - 1))]; + } + return result; +} + +function generateAPIKey() { + var apikey = randomString(16, "@#"); + $("input[name='apiKey']").val(apikey); + return false; +} + +function getJson(str) { + try { + return JSON.parse(str); + } catch (e) { + return false; + } +} + +// ----------------------------------------------------------------------------- +// Actions +// ----------------------------------------------------------------------------- + +function resetOriginals() { + $("input,select").each(function() { + $(this).attr("original", $(this).val()); + }); + numReboot = numReconnect = numReload = 0; +} + +function doReload(milliseconds) { + milliseconds = (typeof milliseconds == "undefined") ? + 0 : + parseInt(milliseconds, 10); + setTimeout(function() { + window.location.reload(); + }, milliseconds); +} + +function doUpgrade() { + + var contents = $("input[name='upgrade']")[0].files[0]; + if (typeof contents === "undefined") { + alert("First you have to select a file from your computer."); + return false; + } + var filename = $("input[name='upgrade']").val().split("\\").pop(); + + var data = new FormData(); + data.append("upgrade", contents, filename); + + $.ajax({ + + // Your server script to process the upload + url: webhost + "upgrade", + type: "POST", + + // Form data + data: data, + + // Tell jQuery not to process data or worry about content-type + // You *must* include these options! + cache: false, + contentType: false, + processData: false, + + success: function(data, text) { + $("#upgrade-progress").hide(); + if (data === "OK") { + alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds."); + doReload(5000); + } else { + alert("There was an error trying to upload the new image, please try again (" + data + ")."); + } + }, + + // Custom XMLHttpRequest + xhr: function() { + $("#upgrade-progress").show(); + var myXhr = $.ajaxSettings.xhr(); + if (myXhr.upload) { + // For handling the progress of the upload + myXhr.upload.addEventListener("progress", function(e) { + if (e.lengthComputable) { + $("progress").attr({ value: e.loaded, max: e.total }); + } + } , false); + } + return myXhr; + } + + }); + + return false; + +} + +function doUpdatePassword() { + var form = $("#formPassword"); + if (validateForm(form)) { + var data = getData(form); + websock.send(JSON.stringify({"config": data})); + } + return false; +} + +function doReboot(ask) { + + var response; + + ask = (typeof ask == "undefined") ? true : ask; + + if (numChanged > 0) { + response = window.confirm("Some changes have not been saved yet, do you want to save them first?"); + if (response === true) { + return doUpdate(); + } + } + + if (ask) { + response = window.confirm("Are you sure you want to reboot the device?"); + if (response === false) { + return false; + } + } + + websock.send(JSON.stringify({"action": "reboot"})); + doReload(5000); + return false; + +} + +function doReconnect(ask) { + var response; + + ask = (typeof ask == "undefined") ? true : ask; + + if (numChanged > 0) { + response = window.confirm("Some changes have not been saved yet, do you want to save them first?"); + if (response === true) { + return doUpdate(); + } + } + + if (ask) { + response = window.confirm("Are you sure you want to disconnect from the current WIFI network?"); + if (response === false) { + return false; + } + } + + websock.send(JSON.stringify({"action": "reconnect"})); + doReload(5000); + return false; + +} + +function doUpdate() { + + var form = $("#formSave"); + if (validateForm(form)) { + + // Get data + var data = getData(form); + websock.send(JSON.stringify({"config": data})); + + // Empty special fields + $(".pwrExpected").val(0); + $("input[name='pwrResetCalibration']"). + prop("checked", false). + iphoneStyle("refresh"); + + // Change handling + numChanged = 0; + setTimeout(function() { + + var response; + + if (numReboot > 0) { + response = window.confirm("You have to reboot the board for the changes to take effect, do you want to do it now?"); + if (response === true) { doReboot(false); } + } else if (numReconnect > 0) { + response = window.confirm("You have to reconnect to the WiFi for the changes to take effect, do you want to do it now?"); + if (response === true) { doReconnect(false); } + } else if (numReload > 0) { + response = window.confirm("You have to reload the page to see the latest changes, do you want to do it now?"); + if (response === true) { doReload(); } + } + + resetOriginals(); + + }, 1000); + + } + + return false; + +} + +function doBackup() { + document.getElementById("downloader").src = webhost + "config"; + return false; +} + +function onFileUpload(event) { + + var inputFiles = this.files; + if (inputFiles === undefined || inputFiles.length === 0) { + return false; + } + var inputFile = inputFiles[0]; + this.value = ""; + + var response = window.confirm("Previous settings will be overwritten. Are you sure you want to restore this settings?"); + if (response === false) { + return false; + } + + var reader = new FileReader(); + reader.onload = function(e) { + var data = getJson(e.target.result); + if (data) { + websock.send(JSON.stringify({"action": "restore", "data": data})); + } else { + alert(messages[4]); + } + }; + reader.readAsText(inputFile); + + return false; + +} + +function doRestore() { + if (typeof window.FileReader !== "function") { + alert("The file API isn't supported on this browser yet."); + } else { + $("#uploader").click(); + } + return false; +} + +function doFactoryReset() { + var response; + + ask = (typeof ask == "undefined") ? true : ask; + + if (numChanged > 0) { + response = window.confirm("Some changes have not been saved yet, do you want to save them first?"); + if (response === true) { + return doUpdate(); + } + } + + if (ask) { + response = window.confirm("Are you sure you want to restore to factory settings?"); + if (response === false) { + return false; + } + } + + websock.send(JSON.stringify({"action": "factory_reset"})); + doReload(5000); + return false; +} + +function doToggle(element, value) { + var relayID = parseInt(element.attr("data"), 10); + websock.send(JSON.stringify({"action": "relay", "data": { "id": relayID, "status": value ? 1 : 0 }})); + return false; +} + +function doScan() { + $("#scanResult").html(""); + $("div.scan.loading").show(); + websock.send(JSON.stringify({"action": "scan", "data": {}})); + return false; +} + +// ----------------------------------------------------------------------------- +// Visualization +// ----------------------------------------------------------------------------- + +function toggleMenu() { + $("#layout").toggleClass("active"); + $("#menu").toggleClass("active"); + $("#menuLink").toggleClass("active"); +} + +function showPanel() { + $(".panel").hide(); + $("#" + $(this).attr("data")).show(); + if ($("#layout").hasClass("active")) { toggleMenu(); } + $("input[type='checkbox']"). + iphoneStyle("calculateDimensions"). + iphoneStyle("refresh"); +} + +// ----------------------------------------------------------------------------- +// Relays & magnitudes mapping +// ----------------------------------------------------------------------------- + +function createRelayList(data, container, template_name) { + + var current = $("#" + container + " > div").length; + if (current > 0) { + return; + } + + var template = $("#" + template_name + " .pure-g")[0]; + for (var i=0; i div").length; + if (current > 0) { + return; + } + + var template = $("#" + template_name + " .pure-g")[0]; + for (var i=0; i div").length; + if (numNetworks >= maxNetworks) { + alert("Max number of networks reached"); + return null; + } + + var tabindex = 200 + numNetworks * 10; + var template = $("#networkTemplate").children(); + var line = $(template).clone(); + $(line).find("input").each(function() { + $(this).attr("tabindex", tabindex); + tabindex++; + }); + $(line).find(".button-del-network").on("click", delNetwork); + $(line).find(".button-more-network").on("click", moreNetwork); + line.appendTo("#networks"); + + return line; + +} + +// ----------------------------------------------------------------------------- +// Relays scheduler +// ----------------------------------------------------------------------------- +function delSchedule() { + var parent = $(this).parents(".pure-g"); + $(parent).remove(); +} + +function moreSchedule() { + var parent = $(this).parents(".pure-g"); + $("div.more", parent).toggle(); +} + +function addSchedule() { + var numSchedules = $("#schedules > div").length; + if (numSchedules >= maxSchedules) { + alert("Max number of schedules reached"); + return null; + } + var tabindex = 200 + numSchedules * 10; + var template = $("#scheduleTemplate").children(); + var line = $(template).clone(); + $(line).find("input").each(function() { + $(this).attr("tabindex", tabindex); + tabindex++; + }); + $(line).find(".button-del-schedule").on("click", delSchedule); + $(line).find(".button-more-schedule").on("click", moreSchedule); + line.appendTo("#schedules"); + return line; +} + +// ----------------------------------------------------------------------------- +// Relays +// ----------------------------------------------------------------------------- + +function initRelays(data) { + + var current = $("#relays > div").length; + if (current > 0) { + return; + } + + var template = $("#relayTemplate .pure-g")[0]; + for (var i=0; i").attr("value",i).text("Switch #" + i)); + + } + + +} + +function initRelayConfig(data) { + + var current = $("#relayConfig > div").length; + if (current > 0) { + return; + } + + var template = $("#relayConfigTemplate").children(); + for (var i=0; i < data.length; i++) { + var line = $(template).clone(); + $("span.gpio", line).html(data[i].gpio); + $("span.id", line).html(i); + $("select[name='relayBoot']", line).val(data[i].boot); + $("select[name='relayPulse']", line).val(data[i].pulse); + $("input[name='relayTime']", line).val(data[i].pulse_ms); + $("input[name='mqttGroup']", line).val(data[i].group); + $("select[name='mqttGroupInv']", line).val(data[i].group_inv); + line.appendTo("#relayConfig"); + } + +} + +// ----------------------------------------------------------------------------- +// Sensors & Magnitudes +// ----------------------------------------------------------------------------- + +function initMagnitudes(data) { + + // check if already initialized + var done = $("#magnitudes > div").length; + if (done > 0) { + return; + } + + // add templates + var template = $("#magnitudeTemplate").children(); + for (var i=0; i div").length; + if (done > 0) { + return; + } + + // add template + var template = $("#colorRGBTemplate").children(); + var line = $(template).clone(); + line.appendTo("#colors"); + + // init color wheel + $("input[name='color']").wheelColorPicker({ + sliders: "wrgbp" + }).on("sliderup", function() { + var value = $(this).wheelColorPicker("getValue", "css"); + websock.send(JSON.stringify({"action": "color", "data" : {"rgb": value}})); + }); + + // init bright slider + $("#brightness").on("change", function() { + var value = $(this).val(); + var parent = $(this).parents(".pure-g"); + $("span", parent).html(value); + websock.send(JSON.stringify({"action": "color", "data" : {"brightness": value}})); + }); + +} + +function initColorHSV() { + + // check if already initialized + var done = $("#colors > div").length; + if (done > 0) { + return; + } + + // add template + var template = $("#colorHSVTemplate").children(); + var line = $(template).clone(); + line.appendTo("#colors"); + + // init color wheel + $("input[name='color']").wheelColorPicker({ + sliders: "whsvp" + }).on("sliderup", function() { + var color = $(this).wheelColorPicker("getColor"); + var value = parseInt(color.h * 360, 10) + "," + parseInt(color.s * 100, 10) + "," + parseInt(color.v * 100, 10); + websock.send(JSON.stringify({"action": "color", "data" : {"hsv": value}})); + }); + +} + +function initChannels(num) { + + // check if already initialized + var done = $("#channels > div").length > 0; + if (done) { + return; + } + + // does it have color channels? + var colors = $("#colors > div").length > 0; + + // calculate channels to create + var max = num; + if (colors) { + max = num % 3; + if ((max > 0) & useWhite) max--; + } + var start = num - max; + + // add templates + var template = $("#channelTemplate").children(); + for (var i=0; i legend").length; + + var template = $("#rfbNodeTemplate").children(); + var line = $(template).clone(); + var status = true; + $("span", line).html(numNodes); + $(line).find("input").each(function() { + $(this).attr("data-id", numNodes); + $(this).attr("data-status", status ? 1 : 0); + status = !status; + }); + $(line).find(".button-rfb-learn").on("click", rfbLearn); + $(line).find(".button-rfb-forget").on("click", rfbForget); + $(line).find(".button-rfb-send").on("click", rfbSend); + line.appendTo("#rfbNodes"); + + return line; +} + +function rfbLearn() { + var parent = $(this).parents(".pure-g"); + var input = $("input", parent); + websock.send(JSON.stringify({"action": "rfblearn", "data" : {"id" : input.attr("data-id"), "status": input.attr("data-status")}})); +} + +function rfbForget() { + var parent = $(this).parents(".pure-g"); + var input = $("input", parent); + websock.send(JSON.stringify({"action": "rfbforget", "data" : {"id" : input.attr("data-id"), "status": input.attr("data-status")}})); +} + +function rfbSend() { + var parent = $(this).parents(".pure-g"); + var input = $("input", parent); + websock.send(JSON.stringify({"action": "rfbsend", "data" : {"id" : input.attr("data-id"), "status": input.attr("data-status"), "data": input.val()}})); +} + +// ----------------------------------------------------------------------------- +// Processing +// ----------------------------------------------------------------------------- + +function processData(data) { + + // title + if ("app_name" in data) { + var title = data.app_name; + if ("app_version" in data) { + title = title + " " + data.app_version; + } + $("span[name=title]").html(title); + if ("hostname" in data) { + title = data.hostname + " - " + title; + } + document.title = title; + } + + Object.keys(data).forEach(function(key) { + + var i; + + // --------------------------------------------------------------------- + // Web mode + // --------------------------------------------------------------------- + + if (key ==="webMode") { + password = data.webMode == 1; + $("#layout").toggle(data.webMode === 0); + $("#password").toggle(data.webMode === 1); + } + + // --------------------------------------------------------------------- + // Actions + // --------------------------------------------------------------------- + + if (key === "action") { + if (data.action === "reload") doReload(1000); + return; + } + + // --------------------------------------------------------------------- + // RFBridge + // --------------------------------------------------------------------- + + if (key === "rfbCount") { + for (i=0; i 0 && position === key.length - 7) { + var module = key.slice(0,-7); + $(".module-" + module).show(); + return; + } + + if (key === "now") { + now = data[key]; + ago = 0; + return; + } + + // Pre-process + if (key === "network") { + data.network = data.network.toUpperCase(); + } + if (key === "mqttStatus") { + data.mqttStatus = data.mqttStatus ? "CONNECTED" : "NOT CONNECTED"; + } + if (key === "ntpStatus") { + data.ntpStatus = data.ntpStatus ? "SYNC'D" : "NOT SYNC'D"; + } + if (key === "uptime") { + var uptime = parseInt(data[key], 10); + var seconds = uptime % 60; uptime = parseInt(uptime / 60, 10); + var minutes = uptime % 60; uptime = parseInt(uptime / 60, 10); + var hours = uptime % 24; uptime = parseInt(uptime / 24, 10); + var days = uptime; + data[key] = days + "d " + zeroPad(hours, 2) + "h " + zeroPad(minutes, 2) + "m " + zeroPad(seconds, 2) + "s"; + } + + // --------------------------------------------------------------------- + // Matching + // --------------------------------------------------------------------- + + var pre; + var post; + + // Look for INPUTs + var input = $("input[name='" + key + "']"); + if (input.length > 0) { + if (input.attr("type") === "checkbox") { + input. + prop("checked", data[key]). + iphoneStyle("refresh"); + } else if (input.attr("type") === "radio") { + input.val([data[key]]); + } else { + pre = input.attr("pre") || ""; + post = input.attr("post") || ""; + input.val(pre + data[key] + post); + } + } + + // Look for SPANs + var span = $("span[name='" + key + "']"); + if (span.length > 0) { + pre = span.attr("pre") || ""; + post = span.attr("post") || ""; + span.html(pre + data[key] + post); + } + + // Look for SELECTs + var select = $("select[name='" + key + "']"); + if (select.length > 0) { + select.val(data[key]); + } + + }); + + // Auto generate an APIKey if none defined yet + if ($("input[name='apiKey']").val() === "") { + generateAPIKey(); + } + + resetOriginals(); + +} + +function hasChanged() { + + var newValue, originalValue; + if ($(this).attr("type") === "checkbox") { + newValue = $(this).prop("checked"); + originalValue = $(this).attr("original") == "true"; + } else { + newValue = $(this).val(); + originalValue = $(this).attr("original"); + } + var hasChanged = $(this).attr("hasChanged") || 0; + var action = $(this).attr("action"); + + if (typeof originalValue === "undefined") { return; } + if (action === "none") { return; } + + if (newValue !== originalValue) { + if (hasChanged === 0) { + ++numChanged; + if (action === "reconnect") ++numReconnect; + if (action === "reboot") ++numReboot; + if (action === "reload") ++numReload; + $(this).attr("hasChanged", 1); + } + } else { + if (hasChanged === 1) { + --numChanged; + if (action === "reconnect") --numReconnect; + if (action === "reboot") --numReboot; + if (action === "reload") --numReload; + $(this).attr("hasChanged", 0); + } + } + +} + +// ----------------------------------------------------------------------------- +// Init & connect +// ----------------------------------------------------------------------------- + +function connect(host) { + + if (typeof host === "undefined") { + host = window.location.href.replace("#", ""); + } else { + if (host.indexOf("http") !== 0) { + host = "http://" + host + "/"; + } + } + if (host.indexOf("http") !== 0) {return;} + + webhost = host; + wshost = host.replace("http", "ws") + "ws"; + + if (websock) websock.close(); + websock = new WebSocket(wshost); + websock.onmessage = function(evt) { + var data = getJson(evt.data); + if (data) processData(data); + }; +} + +$(function() { + + initMessages(); + loadTimeZones(); + setInterval(function() { keepTime(); }, 1000); + + $("#menuLink").on("click", toggleMenu); + $(".pure-menu-link").on("click", showPanel); + $("progress").attr({ value: 0, max: 100 }); + + $(".button-update").on("click", doUpdate); + $(".button-update-password").on("click", doUpdatePassword); + $(".button-reboot").on("click", doReboot); + $(".button-reconnect").on("click", doReconnect); + $(".button-wifi-scan").on("click", doScan); + $(".button-settings-backup").on("click", doBackup); + $(".button-settings-restore").on("click", doRestore); + $(".button-settings-factory").on("click", doFactoryReset); + $("#uploader").on("change", onFileUpload); + $(".button-upgrade").on("click", doUpgrade); + + $(".button-apikey").on("click", generateAPIKey); + $(".button-upgrade-browse").on("click", function() { + $("input[name='upgrade']")[0].click(); + return false; + }); + $("input[name='upgrade']").change(function (){ + var fileName = $(this).val(); + $("input[name='filename']").val(fileName.replace(/^.*[\\\/]/, "")); + }); + $(".button-add-network").on("click", function() { + $(".more", addNetwork()).toggle(); + }); + $(".button-add-schedule").on("click", addSchedule); + + $(document).on("change", "input", hasChanged); + $(document).on("change", "select", hasChanged); + + connect(); + +}); diff --git a/code/html/index.html b/code/html/index.html index 4dd0a5e9..918830ca 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -1,1248 +1,1248 @@ - - - - - ESPurna 0.0.0 - - - - - - - - - - - - - - - - - -
- -
- -
- -
- -
-

SECURITY

-

Before using this device you have to change the default password for the user 'admin'. This password will be used for the AP mode hotspot, the web interface (where you are now) and the over-the-air updates.

-
- -
- -
- -
- - -
-
- The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).
- It must have at least five characters (numbers and letters and any of these special characters: _,.;:~!?@#$%^&*<>\|(){}[]) and at least one lowercase and one uppercase or one number.
-
- -
- - -
- -
- - -
-
-
- -
- -
- -
- -
- - - - - - - -
- -
- -
-

STATUS

-

Current configuration

-
- -
- -
-
- -
- -
- -
- -
- -
- -
Manufacturer
-
- -
Device
-
- -
Chip ID
-
- -
MAC
-
- -
Network
-
- -
BSSID
-
- -
Channel
-
- -
RSSI
-
()
- -
IP
-
- -
Firmware name
-
- -
Firmware version
-
- -
Firmware build
-
- -
Current time
-
- -
Uptime
-
- -
Free heap
-
- -
Firmware size
-
- -
Free space
-
- -
MQTT Status
-
NOT AVAILABLE
- -
NTP Status
-
NOT AVAILABLE
- -
Last update
-
? seconds ago
- -
- -
- -
-
-
- -
- -
- -
-

GENERAL

-

General configuration values

-
- -
- -
- -
- - -
-
-
- This name will identify this device in your network (http://<hostname>.local). For this setting to take effect you should restart the wifi interface clicking the "Reconnect" button. -
-
- -
- - -
-
-
- Delay in milliseconds to detect a double click (from 0 to 1000ms).
- The lower this number the faster the device will respond to button clicks but the harder it will be to get a double click. - Increase this number if you are having trouble to double click the button. - Set this value to 0 to disable double click. You won't be able to set the device in AP mode manually but your device will respond immediately to button clicks.
- You will have to reboot the device after updating for this setting to apply. -
-
- -
- - -
-
-
- This setting defines the behaviour of the main LED in the board.
- When in "WiFi status" it will blink at 1Hz when trying to connecting. If successfully connected if will briefly lit every 5 seconds if in STA mode or every second if in AP mode.
- When in "MQTT managed" mode you will be able to set the LED state sending a message to "<base_topic>/led/0/set" with a payload of 0, 1 or 2 (to toggle it).
- When in "Find me" mode the LED will be ON when all relays are OFF. This is meant to locate switches at night.
- When in "Status" mode the LED will be ON whenever any relay is ON, and OFF otherwise. This is global status notification.
- When in "Mixed" mode it will follow the WiFi status but will stay mostly on when relays are OFF, and mostly OFF when any of them is ON.
- "Always ON" and "Always OFF" modes are self-explanatory. -
-
- -
- -
-
- -
- -
-
-
-
- Home Assistant auto-discovery feature. Enable and save to add the device to your HA console. - When using a colour light you might want to disable CSS style so Home Assistant can parse the color. -
-
- -
- - -
- -
-
-
- -
- -
-

SWITCHES

-

Switch / relay configuration

-
- -
- -
- - General - -
- - -
-
Define how the different switches should be synchronized.
-
- -
- -
- -
-
- -
- -
-

LIGHTS

-

Lights configuration

-
- -
- -
- -
- -
-
-
-
Use color picker for the first 3 channels as RGB.
Will only work if the device has at least 3 dimmable channels.
Reload the page to update the web interface.
-
- -
- -
-
-
-
Use RGB color picker if enabled (plus brightness), otherwise use HSV (hue-saturation-value) style
-
- -
- -
-
-
-
Use forth dimmable channel as white when first 3 have the same RGB value.
Will only work if the device has at least 4 dimmable channels.
Reload the page to update the web interface.
-
- -
- -
-
-
-
Use gamma correction for RGB channels.
Will only work if "use colorpicker" above is also ON.
-
- -
- -
-
-
-
Use CSS style to report colors to MQTT and REST API.
Red will be reported as "#FF0000" if ON, otherwise "255,0,0"
-
- -
- -
-
-
-
If enabled color changes will be smoothed.
-
- -
-
-
-
-
Sync color between different lights.
-
- -
-
-
- -
- -
-

ADMINISTRATION

-

Device administration and security settings

-
- -
- -
- -
- - -
-
- The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).
- It must have at least five characters (numbers and letters and any of these special characters: _,.;:~!?@#$%^&*<>\|(){}[]) and at least one lowercase and one uppercase or one number.
-
- -
- - -
- -
- - -
-
-
- This is the port for the web interface and API requests. - If different than 80 (standard HTTP port) you will have to add it explicitly to your requests: http://myip:myport/ -
-
- -
- -
-
- -
- -
-
-
-
- By default, some magnitudes are being preprocessed and filtered to avoid spurious values. - If you want to get real-time values (not preprocessed) in the API turn on this setting. -
-
- -
- - -
-
-
- This is the key you will have to pass with every HTTP request to the API, either to get or write values. - All API calls must contain the apikey parameter with the value above. - To know what APIs are enabled do a call to /apis. -
-
- -
- -
-
-
-
Turn ON to be able to telnet to your device while connected to your home router.
TELNET is always enabled in AP mode.
-
- - -
- -
-
- -
- - -
-
This name address of the NoFUSS server for automatic remote updates (see https://bitbucket.org/xoseperez/nofuss).
-
- -
- -
-
-
-
- -
- - -
-
-
-
- -
- -
-
-
- -
- -
-

WIFI

-

You can configure up to 5 different WiFi networks. The device will try to connect in order of signal strength.

-
- -
- -
- - General - -
- -
-
-
-
- ESPurna will scan for visible WiFi SSIDs and try to connect to networks defined below in order of signal strength, even if multiple AP share the same SSID. - When disabled, ESPurna will try to connect to the networks in the same order they are listed below. - Disable this option if you are connecting to a single access point (or router) or to a hidden SSID. - -
-
- -
-
- -
-
- -
- - Networks - -
- - - -
-
-
- -
- -
-

SCHEDULE

-

Turn switches ON and OFF based on the current time.

-
- -
- -
- -
- - - -
- -
- -
- -
- -
-

MQTT

-

Configure an MQTT broker in your network and you will be able to change the switch status via an MQTT message.

-
- -
- -
- -
- -
-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- If left empty the firmware will generate a client ID based on the serial number of the chip. -
-
- -
- - -
- -
- -
-
- -
- - -
- -
- -
-
- -
- - -
-
- This is the fingerprint for the SSL certificate of the server.
- You can get it using https://www.grc.com/fingerprints.htm
- or using openssl from a linux box by typing:
-
$ openssl s_client -connect <host>:<port> < /dev/null 2>/dev/null | openssl x509 -fingerprint -noout -in /dev/stdin
-
-
- -
- - -
-
- This is the root topic for this device. The {hostname} and {mac} placeholders will be replaced by the device hostname and MAC address.
- - <root>/relay/#/set Send a 0 or a 1 as a payload to this topic to switch it on or off. You can also send a 2 to toggle its current state. Replace # with the switch ID (starting from 0). If the board has only one switch it will be 0.
- - <root>/rgb/set Set the color using this topic, your can either send an "#RRGGBB" value or "RRR,GGG,BBB" (0-255 each).
- - <root>/hsv/set Set the color using hue (0-360), saturation (0-100) and value (0-100) values, comma separated.
- - <root>/brightness/set Set the brighness (0-255).
- - <root>/channel/#/set Set the value for a single color channel (0-255). Replace # with the channel ID (starting from 0 and up to 4 for RGBWC lights).
- - <root>/mired/set Set the temperature color in mired.
- - <root>/status The device will report a 1 to this topic every few minutes. Upon MQTT disconnecting this will be set to 0.
- - Other values reported (depending on the build) are: firmware and version, hostname, IP, MAC, signal strenth (RSSI), uptime (in seconds), free heap and power supply. -
-
- -
- -
-
-
- All messages (except the device status) will be included in a JSON payload along with the timestamp and hostname - and sent under the <root>/data topic.
- Messages will be queued and sent after 100ms, so different messages could be merged into a single payload.
- Subscribtions will still be done to single topics. -
-
- -
-
- -
- -
- -
-

NTP

-

Configure your NTP (Network Time Protocol) servers and local configuration to keep your device time up to the second for your location.

-
- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- -
-
- -
- -
- -
-

DOMOTICZ

-

- Configure the connection to your Domoticz server. -

-
- -
- -
- - General - -
- -
-
- -
- - -
- -
- - -
- - Sensors & actuators - -
-
Set IDX to 0 to disable notifications from that component.
-
- -
- -
- -
-
- -
- -
- -
-

THINGSPEAK

-

- Send your sensors data to Thingspeak. -

-
- -
- -
- - General - -
- -
-
- -
- - -
- - Sensors & actuators - -
-
Enter the field number to send each data to, 0 disable notifications from that component.
-
- -
- -
- -
-
- -
- -
- -
-

INFLUXDB

-

- Configure the connection to your InfluxDB server. Leave the host field empty to disable InfluxDB connection. -

-
- -
- -
- -
- -
-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- -
- -
- -
-

SENSOR CONFIGURATION

-

- Configure and calibrate your device sensors. -

-
- -
- -
- - General - -
- - -
-
-
- Select the interval between readings. These will be filtered and averaged for the report. The default and recommended value is 6 seconds. -
-
- -
- -
-
-
-
- Select the number of readings to average and report -
-
- -
- - -
- -
- - -
-
-
- Temperature correction value is added to the measured value which may be inaccurate due to many factors. The value can be negative. -
-
- - Energy monitor - -
- - -
-
Mains voltage in your system (in V).
-
- -
- - -
-
In Ampers (A). If you are using a pure resistive load like a bulb this will the ratio between the two previous values, i.e. power / voltage. You can also use a current clamp around one fo the power wires to get this value.
-
- -
- - -
-
In Volts (V). Enter your the nominal AC voltage for your household or facility, or use multimeter to get this value.
-
- -
- - -
-
In Watts (W). Calibrate your sensor connecting a pure resistive load (like a bulb) and enter here the its nominal power or use a multimeter.
-
- -
- -
-
-
-
Move this switch to ON and press "Save" to revert to factory calibration values.
-
- -
-
- -
- -
- -
-

RFBRIDGE

-

- Sonoff 433 RF Bridge Configuration

- To learn a new code click LEARN, the Sonoff RFBridge will beep, then press a button on the remote, the RFBridge will then double beep and the new code should show up. If the device double beeps but the code does not update it has not been properly learnt. Keep trying.

- Modify or create new codes manually (18 characters) and then click SAVE to store them in the device memory. If your controlled device uses the same code to switch ON and OFF, learn the code with the ON button and copy paste it to the OFF input box, then click SAVE on the last one to store the value.

- Delete any code clicking the FORGET button. -

You can also specify 116-chars long RAW codes. Raw codes require a specific firmware for for the EFM8BB1.
-

-
- -
-
-
-
-
-
- -
- -
- -
- - - -
- - Switch # - -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
- -
- -
- - -
-
- - - - - - -
-
Leave empty for DNS negotiation
- - - -
-
Set when using a static IP
- - - -
-
Usually 255.255.255.0 for /24 networks
- - - -
-
Set the Domain Name Server IP to use when using a static IP
- -
- - -
- -
- -
- -
- - -
- -
 h
-
-
- -
 m
-
-
- - -
- -
-
 1 for Monday, 2 for Tuesday...
- - -
- -
- -
- - -
- -
- - -
- -
- -
-
- -
-
-
- -
- Switch # (GPIO) -
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- -
-
- -
-
-
-
- -
-
- - -
- -
- - - -
-
- -
-
- - -
-
- -
-
- - - -
-
- -
-
- -
- -
-
-
-
- - - - - - - - - - - - - - + + + + + ESPurna 0.0.0 + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+

SECURITY

+

Before using this device you have to change the default password for the user 'admin'. This password will be used for the AP mode hotspot, the web interface (where you are now) and the over-the-air updates.

+
+ +
+ +
+ +
+ + +
+
+ The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).
+ It must have at least five characters (numbers and letters and any of these special characters: _,.;:~!?@#$%^&*<>\|(){}[]) and at least one lowercase and one uppercase or one number.
+
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+ +
+ +
+

STATUS

+

Current configuration

+
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
Manufacturer
+
+ +
Device
+
+ +
Chip ID
+
+ +
MAC
+
+ +
Network
+
+ +
BSSID
+
+ +
Channel
+
+ +
RSSI
+
()
+ +
IP
+
+ +
Firmware name
+
+ +
Firmware version
+
+ +
Firmware build
+
+ +
Current time
+
+ +
Uptime
+
+ +
Free heap
+
+ +
Firmware size
+
+ +
Free space
+
+ +
MQTT Status
+
NOT AVAILABLE
+ +
NTP Status
+
NOT AVAILABLE
+ +
Last update
+
? seconds ago
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+

GENERAL

+

General configuration values

+
+ +
+ +
+ +
+ + +
+
+
+ This name will identify this device in your network (http://<hostname>.local). For this setting to take effect you should restart the wifi interface clicking the "Reconnect" button. +
+
+ +
+ + +
+
+
+ Delay in milliseconds to detect a double click (from 0 to 1000ms).
+ The lower this number the faster the device will respond to button clicks but the harder it will be to get a double click. + Increase this number if you are having trouble to double click the button. + Set this value to 0 to disable double click. You won't be able to set the device in AP mode manually but your device will respond immediately to button clicks.
+ You will have to reboot the device after updating for this setting to apply. +
+
+ +
+ + +
+
+
+ This setting defines the behaviour of the main LED in the board.
+ When in "WiFi status" it will blink at 1Hz when trying to connecting. If successfully connected if will briefly lit every 5 seconds if in STA mode or every second if in AP mode.
+ When in "MQTT managed" mode you will be able to set the LED state sending a message to "<base_topic>/led/0/set" with a payload of 0, 1 or 2 (to toggle it).
+ When in "Find me" mode the LED will be ON when all relays are OFF. This is meant to locate switches at night.
+ When in "Status" mode the LED will be ON whenever any relay is ON, and OFF otherwise. This is global status notification.
+ When in "Mixed" mode it will follow the WiFi status but will stay mostly on when relays are OFF, and mostly OFF when any of them is ON.
+ "Always ON" and "Always OFF" modes are self-explanatory. +
+
+ +
+ +
+
+ +
+ +
+
+
+
+ Home Assistant auto-discovery feature. Enable and save to add the device to your HA console. + When using a colour light you might want to disable CSS style so Home Assistant can parse the color. +
+
+ +
+ + +
+ +
+
+
+ +
+ +
+

SWITCHES

+

Switch / relay configuration

+
+ +
+ +
+ + General + +
+ + +
+
Define how the different switches should be synchronized.
+
+ +
+ +
+ +
+
+ +
+ +
+

LIGHTS

+

Lights configuration

+
+ +
+ +
+ +
+ +
+
+
+
Use color picker for the first 3 channels as RGB.
Will only work if the device has at least 3 dimmable channels.
Reload the page to update the web interface.
+
+ +
+ +
+
+
+
Use RGB color picker if enabled (plus brightness), otherwise use HSV (hue-saturation-value) style
+
+ +
+ +
+
+
+
Use forth dimmable channel as white when first 3 have the same RGB value.
Will only work if the device has at least 4 dimmable channels.
Reload the page to update the web interface.
+
+ +
+ +
+
+
+
Use gamma correction for RGB channels.
Will only work if "use colorpicker" above is also ON.
+
+ +
+ +
+
+
+
Use CSS style to report colors to MQTT and REST API.
Red will be reported as "#FF0000" if ON, otherwise "255,0,0"
+
+ +
+ +
+
+
+
If enabled color changes will be smoothed.
+
+ +
+
+
+
+
Sync color between different lights.
+
+ +
+
+
+ +
+ +
+

ADMINISTRATION

+

Device administration and security settings

+
+ +
+ +
+ +
+ + +
+
+ The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).
+ It must have at least five characters (numbers and letters and any of these special characters: _,.;:~!?@#$%^&*<>\|(){}[]) and at least one lowercase and one uppercase or one number.
+
+ +
+ + +
+ +
+ + +
+
+
+ This is the port for the web interface and API requests. + If different than 80 (standard HTTP port) you will have to add it explicitly to your requests: http://myip:myport/ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+ By default, some magnitudes are being preprocessed and filtered to avoid spurious values. + If you want to get real-time values (not preprocessed) in the API turn on this setting. +
+
+ +
+ + +
+
+
+ This is the key you will have to pass with every HTTP request to the API, either to get or write values. + All API calls must contain the apikey parameter with the value above. + To know what APIs are enabled do a call to /apis. +
+
+ +
+ +
+
+
+
Turn ON to be able to telnet to your device while connected to your home router.
TELNET is always enabled in AP mode.
+
+ + +
+ +
+
+ +
+ + +
+
This name address of the NoFUSS server for automatic remote updates (see https://bitbucket.org/xoseperez/nofuss).
+
+ +
+ +
+
+
+
+ +
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+

WIFI

+

You can configure up to 5 different WiFi networks. The device will try to connect in order of signal strength.

+
+ +
+ +
+ + General + +
+ +
+
+
+
+ ESPurna will scan for visible WiFi SSIDs and try to connect to networks defined below in order of signal strength, even if multiple AP share the same SSID. + When disabled, ESPurna will try to connect to the networks in the same order they are listed below. + Disable this option if you are connecting to a single access point (or router) or to a hidden SSID. + +
+
+ +
+
+ +
+
+ +
+ + Networks + +
+ + + +
+
+
+ +
+ +
+

SCHEDULE

+

Turn switches ON and OFF based on the current time.

+
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ +
+

MQTT

+

Configure an MQTT broker in your network and you will be able to change the switch status via an MQTT message.

+
+ +
+ +
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ If left empty the firmware will generate a client ID based on the serial number of the chip. +
+
+ +
+ + +
+ +
+ +
+
+ +
+ + +
+ +
+ +
+
+ +
+ + +
+
+ This is the fingerprint for the SSL certificate of the server.
+ You can get it using https://www.grc.com/fingerprints.htm
+ or using openssl from a linux box by typing:
+
$ openssl s_client -connect <host>:<port> < /dev/null 2>/dev/null | openssl x509 -fingerprint -noout -in /dev/stdin
+
+
+ +
+ + +
+
+ This is the root topic for this device. The {hostname} and {mac} placeholders will be replaced by the device hostname and MAC address.
+ - <root>/relay/#/set Send a 0 or a 1 as a payload to this topic to switch it on or off. You can also send a 2 to toggle its current state. Replace # with the switch ID (starting from 0). If the board has only one switch it will be 0.
+ - <root>/rgb/set Set the color using this topic, your can either send an "#RRGGBB" value or "RRR,GGG,BBB" (0-255 each).
+ - <root>/hsv/set Set the color using hue (0-360), saturation (0-100) and value (0-100) values, comma separated.
+ - <root>/brightness/set Set the brighness (0-255).
+ - <root>/channel/#/set Set the value for a single color channel (0-255). Replace # with the channel ID (starting from 0 and up to 4 for RGBWC lights).
+ - <root>/mired/set Set the temperature color in mired.
+ - <root>/status The device will report a 1 to this topic every few minutes. Upon MQTT disconnecting this will be set to 0.
+ - Other values reported (depending on the build) are: firmware and version, hostname, IP, MAC, signal strenth (RSSI), uptime (in seconds), free heap and power supply. +
+
+ +
+ +
+
+
+ All messages (except the device status) will be included in a JSON payload along with the timestamp and hostname + and sent under the <root>/data topic.
+ Messages will be queued and sent after 100ms, so different messages could be merged into a single payload.
+ Subscribtions will still be done to single topics. +
+
+ +
+
+ +
+ +
+ +
+

NTP

+

Configure your NTP (Network Time Protocol) servers and local configuration to keep your device time up to the second for your location.

+
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+

DOMOTICZ

+

+ Configure the connection to your Domoticz server. +

+
+ +
+ +
+ + General + +
+ +
+
+ +
+ + +
+ +
+ + +
+ + Sensors & actuators + +
+
Set IDX to 0 to disable notifications from that component.
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+

THINGSPEAK

+

+ Send your sensors data to Thingspeak. +

+
+ +
+ +
+ + General + +
+ +
+
+ +
+ + +
+ + Sensors & actuators + +
+
Enter the field number to send each data to, 0 disable notifications from that component.
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+

INFLUXDB

+

+ Configure the connection to your InfluxDB server. Leave the host field empty to disable InfluxDB connection. +

+
+ +
+ +
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ +
+ +
+

SENSOR CONFIGURATION

+

+ Configure and calibrate your device sensors. +

+
+ +
+ +
+ + General + +
+ + +
+
+
+ Select the interval between readings. These will be filtered and averaged for the report. The default and recommended value is 6 seconds. +
+
+ +
+ +
+
+
+
+ Select the number of readings to average and report +
+
+ +
+ + +
+ +
+ + +
+
+
+ Temperature correction value is added to the measured value which may be inaccurate due to many factors. The value can be negative. +
+
+ + Energy monitor + +
+ + +
+
Mains voltage in your system (in V).
+
+ +
+ + +
+
In Ampers (A). If you are using a pure resistive load like a bulb this will the ratio between the two previous values, i.e. power / voltage. You can also use a current clamp around one fo the power wires to get this value.
+
+ +
+ + +
+
In Volts (V). Enter your the nominal AC voltage for your household or facility, or use multimeter to get this value.
+
+ +
+ + +
+
In Watts (W). Calibrate your sensor connecting a pure resistive load (like a bulb) and enter here the its nominal power or use a multimeter.
+
+ +
+ +
+
+
+
Move this switch to ON and press "Save" to revert to factory calibration values.
+
+ +
+
+ +
+ +
+ +
+

RFBRIDGE

+

+ Sonoff 433 RF Bridge Configuration

+ To learn a new code click LEARN, the Sonoff RFBridge will beep, then press a button on the remote, the RFBridge will then double beep and the new code should show up. If the device double beeps but the code does not update it has not been properly learnt. Keep trying.

+ Modify or create new codes manually (18 characters) and then click SAVE to store them in the device memory. If your controlled device uses the same code to switch ON and OFF, learn the code with the ON button and copy paste it to the OFF input box, then click SAVE on the last one to store the value.

+ Delete any code clicking the FORGET button. +

You can also specify 116-chars long RAW codes. Raw codes require a specific firmware for for the EFM8BB1.
+

+
+ +
+
+
+
+
+
+ +
+ +
+ +
+ + + +
+ + Switch # + +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+ +
+ + +
+
+ + + + + + +
+
Leave empty for DNS negotiation
+ + + +
+
Set when using a static IP
+ + + +
+
Usually 255.255.255.0 for /24 networks
+ + + +
+
Set the Domain Name Server IP to use when using a static IP
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
 h
+
+
+ +
 m
+
+
+ + +
+ +
+
 1 for Monday, 2 for Tuesday...
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+
+
+ +
+ Switch # (GPIO) +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + +
+ +
+ + + +
+
+ +
+
+ + +
+
+ +
+
+ + + +
+
+ +
+
+ +
+ +
+
+
+
+ + + + + + + + + + + + + +