From 335383a1c6e0d60a987d53cf99e2cfbe241f84c0 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Mon, 21 Oct 2019 21:11:29 +0100 Subject: [PATCH 01/18] Python 3 support --- scripts/extra_script.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/extra_script.py b/scripts/extra_script.py index c5b5db8..a843704 100644 --- a/scripts/extra_script.py +++ b/scripts/extra_script.py @@ -23,7 +23,7 @@ def get_c_name(source_file): def text_to_header(source_file): with open(source_file) as source_fh: - original = source_fh.read().decode('utf-8') + original = source_fh.read() filename = get_c_name(source_file) output = "static const char CONTENT_{}[] PROGMEM = ".format(filename) for line in original.splitlines(): @@ -38,7 +38,7 @@ def binary_to_header(source_file): with open(source_file, "rb") as source_fh: byte = source_fh.read(1) - while byte != "": + while byte != b"": output += "0x{:02x}, ".format(ord(byte)) count += 1 if 16 == count: @@ -62,7 +62,7 @@ def data_to_header(env, target, source): target_file = target[0].get_abspath() print("Generating {}".format(target_file)) with open(target_file, "w") as output_file: - output_file.write(output.encode('utf-8')) + output_file.write(output) def make_static(env, target, source): output = "" @@ -103,7 +103,7 @@ def make_static(env, target, source): target_file = target[0].get_abspath() print("Generating {}".format(target_file)) with open(target_file, "w") as output_file: - output_file.write(output.encode('utf-8')) + output_file.write(output) def process_html_app(source, dest, env): web_server_static_files = join(dest, "web_server_static_files.h") From 568695c920d985f64f4ce1a7b2799c9c49a553d8 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Mon, 21 Oct 2019 21:16:50 +0100 Subject: [PATCH 02/18] Handle SVG files --- scripts/extra_script.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/extra_script.py b/scripts/extra_script.py index a843704..c24c7b2 100644 --- a/scripts/extra_script.py +++ b/scripts/extra_script.py @@ -55,7 +55,7 @@ def data_to_header(env, target, source): for source_file in source: #print("Reading {}".format(source_file)) file = source_file.get_abspath() - if file.endswith(".css") or file.endswith(".js") or file.endswith(".htm") or file.endswith(".html"): + if file.endswith(".css") or file.endswith(".js") or file.endswith(".htm") or file.endswith(".html") or file.endswith(".svg"): output += text_to_header(file) else: output += binary_to_header(file) @@ -94,6 +94,8 @@ def make_static(env, target, source): filetype = "JPEG" elif out_file.endswith(".png"): filetype = "PNG" + elif out_file.endswith(".svg"): + filetype = "SVG" c_name = get_c_name(out_file) output += " { \"/"+out_file+"\", CONTENT_"+c_name+", sizeof(CONTENT_"+c_name+") - 1, _CONTENT_TYPE_"+filetype+" },\n" From 4f2c916e95a09d9898fe7aba8b46a7d3516c32e7 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Fri, 12 Jun 2020 22:06:47 +0100 Subject: [PATCH 03/18] Initial backport of divert changes and some other associated features from the ESP32 build --- .gitignore | 3 + gui | 2 +- platformio.ini | 18 +- src/RapiSender.cpp | 284 ------------- src/RapiSender.h | 60 --- src/app_config.cpp | 287 +++++++++++++ src/{config.h => app_config.h} | 38 +- src/app_config_mode.h | 59 +++ src/app_config_v1.cpp | 169 ++++++++ src/config.cpp | 383 ----------------- src/debug.h | 10 +- src/divert.cpp | 160 ++++--- src/divert.h | 4 + src/emoncms.cpp | 66 ++- src/emoncms.h | 5 +- src/emonesp.h | 44 ++ src/event.h | 1 + src/input.cpp | 399 +++++++----------- src/input.h | 21 +- src/mqtt.cpp | 144 ++++--- src/mqtt.h | 3 +- src/ohm.cpp | 26 +- src/openevse.h | 20 - src/ota.cpp | 2 +- src/src.ino | 155 ++++--- src/web_server.cpp | 315 ++++++-------- src/web_server.h | 6 +- src/web_server_static.cpp | 2 +- src/web_static/web_server.assets.js.h | 2 +- src/web_static/web_server.home.html.h | 54 ++- src/web_static/web_server.home.js.h | 2 +- src/web_static/web_server.lib.js.h | 2 +- src/web_static/web_server.style.css.h | 2 +- src/web_static/web_server.wifi_portal.html.h | 2 +- src/web_static/web_server.wifi_portal.js.h | 2 +- src/web_static/web_server.wifi_signal_1.svg.h | 68 +++ src/web_static/web_server.wifi_signal_2.svg.h | 67 +++ src/web_static/web_server.wifi_signal_3.svg.h | 66 +++ src/web_static/web_server.wifi_signal_4.svg.h | 65 +++ src/web_static/web_server.wifi_signal_5.svg.h | 55 +++ src/web_static/web_server_static_files.h | 10 + src/wifi.cpp | 2 +- 42 files changed, 1655 insertions(+), 1430 deletions(-) delete mode 100644 src/RapiSender.cpp delete mode 100644 src/RapiSender.h create mode 100644 src/app_config.cpp rename src/{config.h => app_config.h} (70%) create mode 100644 src/app_config_mode.h create mode 100644 src/app_config_v1.cpp delete mode 100644 src/config.cpp delete mode 100644 src/openevse.h create mode 100644 src/web_static/web_server.wifi_signal_1.svg.h create mode 100644 src/web_static/web_server.wifi_signal_2.svg.h create mode 100644 src/web_static/web_server.wifi_signal_3.svg.h create mode 100644 src/web_static/web_server.wifi_signal_4.svg.h create mode 100644 src/web_static/web_server.wifi_signal_5.svg.h diff --git a/.gitignore b/.gitignore index 324d7c7..69bc38b 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,6 @@ ubuntu-xenial-16.04-cloudimg-console.log .vscode/*.db .vscode/launch.json .vscode/.browse.c_cpp.db* +lib/ConfigJson +lib/ESPAL +lib/OpenEVSE diff --git a/gui b/gui index adb7b9c..6ebd1a5 160000 --- a/gui +++ b/gui @@ -1 +1 @@ -Subproject commit adb7b9c96cdc3d40d6446ba2806198196588856a +Subproject commit 6ebd1a5d58d09092937baa4fa58f4476fb1cfc2a diff --git a/platformio.ini b/platformio.ini index 4be18c5..6fead1c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,17 +32,26 @@ default_envs = openevse [common] version = -DBUILD_TAG=2.8.1 monitor_speed=115200 -lib_deps = PubSubClient@2.6, ESP Async WebServer@1.1.1, ESPAsyncTCP@1.1.3 +lib_deps = + PubSubClient@2.6 + ESP Async WebServer@1.1.1 + ESPAsyncTCP@1.1.3 + ArduinoJson@6.15.1 + Micro Debug@0.0.3 + ConfigJson@0.0.3 + OpenEVSE@0.0.2 + ESPAL@0.0.1 extra_scripts = scripts/extra_script.py debug_flags = -DENABLE_DEBUG -DENABLE_PROFILE -DDEBUG_PORT=Serial1 ota_flags = -DENABLE_OTA -DWIFI_LED=0 build_flags = + -DENABLE_DEBUG # -DENABLE_ASYNC_WIFI_SCAN # specify exact Arduino ESP SDK version, requires platformio 3.5+ (curently dev version) # http://docs.platformio.org/en/latest/projectconf/section_env_general.html#platform platform = https://github.com/platformio/platform-espressif8266.git#release/v1.6.0 -platform_stage = https://github.com/platformio/platform-espressif8266.git#feature/stage +platform_stage = https://github.com/platformio/platform-espressif8266.git#develop [env:openevse] platform = ${common.platform} @@ -51,7 +60,7 @@ framework = arduino lib_deps = ${common.lib_deps} src_build_flags = ${common.version} ${common.build_flags} # Upload at faster baud: takes 20s instead of 50s. Use 'pio run -t upload -e evse_slow to use slower default baud rate' -upload_speed=921600 +upload_speed = 921600 monitor_speed = ${common.monitor_speed} extra_scripts = ${common.extra_scripts} @@ -81,7 +90,8 @@ board = esp12e framework = arduino lib_deps = ${common.lib_deps} src_build_flags = ${common.version}.dev ${common.build_flags} ${common.ota_flags} ${common.debug_flags} -upload_speed=921600 +# Upload at faster baud: takes 20s instead of 50s. Use 'pio run -t upload -e evse_slow to use slower default baud rate' +upload_speed = 921600 monitor_speed = ${common.monitor_speed} extra_scripts = ${common.extra_scripts} diff --git a/src/RapiSender.cpp b/src/RapiSender.cpp deleted file mode 100644 index 46a1a4e..0000000 --- a/src/RapiSender.cpp +++ /dev/null @@ -1,284 +0,0 @@ -#if defined(ENABLE_DEBUG) && !defined(ENABLE_DEBUG_RAPI) -#undef ENABLE_DEBUG -#endif - -#include -#include "RapiSender.h" -#include "debug.h" - -#define dbgprint(s) DBUG(s) -#define dbgprintln(s) DBUGLN(s) -#ifdef ENABLE_DEBUG -#define DBG -#endif - -// convert 2-digit hex string to uint8_t -uint8_t -htou8(const char *s) { - uint8_t u = 0; - for (int i = 0; i < 2; i++) { - char c = s[i]; - if (c != '\0') { - if (i == 1) - u <<= 4; - if ((c >= '0') && (c <= '9')) { - u += c - '0'; - } else if ((c >= 'A') && (c <= 'F')) { - u += c - 'A' + 10; - } - // else if ((c >= 'a') && (c <= 'f')) { - // u += c - 'a' + 10; - // } - else { - // invalid character received - return 0; - } - } - } - return u; -} - - -RapiSender::RapiSender(Stream * stream) { - _stream = stream; - *_respBuf = 0; - _flags = 0; - _onRapiEvent = nullptr; - - _sequenceId = RAPI_INVALID_SEQUENCE_ID; -} - -// return = 0 = OK -// = 1 = command will cause buffer overflow -void -RapiSender::_sendCmd(const char *cmdstr) { - _stream->print(cmdstr); - dbgprint(cmdstr); - - const char *s = cmdstr; - uint8_t chk = 0; - while (*s) { - chk ^= *(s++); - } - - _sendTail(chk); -} - -void RapiSender::_sendTail(uint8_t chk) { - if (_sequenceIdEnabled()) { - if (++_sequenceId == RAPI_INVALID_SEQUENCE_ID) - ++_sequenceId; - sprintf(_respBuf, " %c%02X", ESRAPI_SOS, (unsigned) _sequenceId); - const char *s = _respBuf; - while (*s) { - chk ^= *(s++); - } - _stream->print(_respBuf); - dbgprint(_respBuf); - } - - sprintf(_respBuf, "^%02X%c", (unsigned) chk, ESRAPI_EOC); - _stream->print(_respBuf); - dbgprintln(_respBuf); - _stream->flush(); - - *_respBuf = 0; -} - -// return = 0 = OK -// = 1 = bad checksum -// = 2 = bad sequence id -int -RapiSender::_tokenize() { - uint8_t chk = 0; - char *s = _respBuf; - dbgprint("resp: "); - dbgprintln(_respBuf); - - while (*s != '^' && *s != '\0') { - chk ^= *(s++); - } - if (*s == '^') { - uint8_t rchk = htou8(s + 1); - if (rchk != chk) { - _tokenCnt = 0; -#ifdef DBG - sprintf(_respBuf, "bad chk %x %x %s", rchk, chk, s); - dbgprintln(_respBuf); -#endif - return 1; - } - *s = '\0'; - } - - _tokenCnt = 0; - s = _respBuf; - while (*s) { - _tokens[_tokenCnt++] = s++; - if (_tokenCnt == RAPI_MAX_TOKENS) - break; - - while (*s && (*s != ' ') && (*s != ESRAPI_SOS)) { - s++; - } - if (*s == ' ') { - *(s++) = '\0'; - } else if (*s == ESRAPI_SOS) { - *(s++) = '\0'; - uint8_t seqid = htou8(s + 1); - if (seqid != _sequenceId) { -#ifdef DBG - sprintf(_respBuf, "bad seqid %x %x %s", seqid, _sequenceId, s); - dbgprintln(_respBuf); -#endif - _tokenCnt = 0; - return 2; - } - break; // sequence id is last - break out - } - } - - return 0; -} - -/* - * return values: - * -1= timeout - * 0= success - * 1=$NK - * 2=invalid RAPI response - * 3=cmdstr too long -*/ -int -RapiSender::sendCmd(const char *cmdstr, unsigned long timeout) { - _sendCmd(cmdstr); - return _waitForResult(timeout); -} - -/* - * return values: - * -1= timeout - * 0= success - * 1=$NK - * 2=invalid RAPI response - * 3=cmdstr too long -*/ -int -RapiSender::sendCmd(String &cmdstr, unsigned long timeout) { - _sendCmd(cmdstr.c_str()); - return _waitForResult(timeout); -} - -/* - * return values: - * -1= timeout - * 0= success - * 1=$NK - * 2=invalid RAPI response - * 3=cmdstr too long -*/ -int -RapiSender::sendCmd(const __FlashStringHelper *cmdstr, unsigned long timeout) { - _stream->print(cmdstr); - dbgprint(cmdstr); - - PGM_P p = reinterpret_cast(cmdstr); - uint8_t chk = 0; - while (1) { - uint8_t c = pgm_read_byte(p++); - if (c == 0) break; - chk ^= c; - } - - _sendTail(chk); - return _waitForResult(timeout); -} - -/* - * return values: - * -1= timeout - * 0= success - * 1=$NK - * 2=invalid RAPI response - * 3=cmdstr too long -*/ -int -RapiSender::_waitForResult(unsigned long timeout) { - unsigned long mss = millis(); -start: - _tokenCnt = 0; - *_respBuf = 0; - int bufpos = 0; - do { - int bytesavail = _stream->available(); - if (bytesavail) { - for (int i = 0; i < bytesavail; i++) { - char c = _stream->read(); - if (!bufpos && (c != ESRAPI_SOC)) { - // wait for start character - continue; - } else if (bufpos && (c == ESRAPI_EOC)) { - _respBuf[bufpos] = '\0'; - // Save the original response - strncpy(_respBufOrig, _respBuf, RAPI_BUFLEN); - if (!_tokenize()) - break; - else - goto start; - } else { - _respBuf[bufpos++] = c; - if (bufpos >= (RAPI_BUFLEN - 1)) - return -2; - } - } - } - } while (!_tokenCnt && ((millis() - mss) < timeout)); - -#ifdef DBG - dbgprint("TOKENCNT: "); - dbgprintln(_tokenCnt); - for (int i = 0; i < _tokenCnt; i++) { - dbgprintln(_tokens[i]); - } - dbgprintln(""); -#endif - - if (_tokenCnt > 0) { - if (!strcmp(_tokens[0], "$OK")) { - return 0; - } else if (!strcmp(_tokens[0], "$NK")) { - return 1; - } else if (!strcmp(_tokens[0],"$WF") || - !strcmp(_tokens[0],"$ST")) - { - // async EVSE state transition or WiFi event - if(nullptr != _onRapiEvent) { - _onRapiEvent(); - } - goto start; - } else { // not OK or NK - return 2; - } - } else { // !_tokenCnt - return -1; - } -} - -void -RapiSender::enableSequenceId(uint8_t tf) { - if (tf) { - _sequenceId = (uint8_t) millis(); // seed with random number - _flags |= RSF_SEQUENCE_ID_ENABLED; - } else { - _sequenceId = RAPI_INVALID_SEQUENCE_ID; - _flags &= ~RSF_SEQUENCE_ID_ENABLED; - } -} - -void -RapiSender::loop() -{ - if(_stream->available()) { - _waitForResult(RAPI_TIMEOUT_MS); - } -} diff --git a/src/RapiSender.h b/src/RapiSender.h deleted file mode 100644 index baad39b..0000000 --- a/src/RapiSender.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once -#include - -// only enable if RAPI ver -#define RAPI_SEQUENCE_ID - -#define RAPI_INVALID_SEQUENCE_ID 0 - -#define RAPI_TIMEOUT_MS 500 -#define RAPI_BUFLEN 40 -#define RAPI_MAX_TOKENS 10 - -#define ESRAPI_SOC '$' // start of command -#define ESRAPI_EOC 0xd // CR end of command -#define ESRAPI_SOS ':' // start of sequence id - -// _flags -#define RSF_SEQUENCE_ID_ENABLED 0x01 - -typedef void (* fnRapiEvent)(); - -class RapiSender { - Stream *_stream; - uint8_t _sequenceId; - uint8_t _flags; - int _tokenCnt; - char *_tokens[RAPI_MAX_TOKENS]; - fnRapiEvent _onRapiEvent; - - char _respBuf[RAPI_BUFLEN]; - char _respBufOrig[RAPI_BUFLEN]; - int _tokenize(); - void _sendCmd(const char *cmdstr); - void _sendTail(uint8_t chk); - int _waitForResult(unsigned long timeout); - uint8_t _sequenceIdEnabled() { - return (_flags & RSF_SEQUENCE_ID_ENABLED) ? 1 : 0; - } - -public: - - RapiSender(Stream *stream); - void setStream(Stream *stream) { _stream = stream; } - // void sendString(const char *str) { dbgprint(str); } - int sendCmd(const char *cmdstr, unsigned long timeout=RAPI_TIMEOUT_MS); - int sendCmd(String &cmdstr, unsigned long timeout=RAPI_TIMEOUT_MS); - int sendCmd(const __FlashStringHelper *cmdstr, unsigned long timeout=RAPI_TIMEOUT_MS); - void enableSequenceId(uint8_t tf); - int8_t getTokenCnt() { return _tokenCnt; } - const char *getResponse() { return _respBufOrig; } - const char *getToken(int i) { - if (i < _tokenCnt) return _tokens[i]; - else return NULL; - } - void setOnEvent(fnRapiEvent callback) { - _onRapiEvent = callback; - } - void loop(); -}; - diff --git a/src/app_config.cpp b/src/app_config.cpp new file mode 100644 index 0000000..0937f53 --- /dev/null +++ b/src/app_config.cpp @@ -0,0 +1,287 @@ +#include "emonesp.h" +#include "espal.h" +#include "divert.h" +#include "mqtt.h" +#include "emoncms.h" +#include "input.h" + +#include "app_config.h" +#include "app_config_mode.h" + +#include +#include // Save config settings +#include + +#define EEPROM_SIZE 4096 +#define CHECKSUM_SEED 128 + +// Wifi Network Strings +String esid; +String epass; + +// Web server authentication (leave blank for none) +String www_username; +String www_password; + +// Advanced settings +String esp_hostname; +String sntp_hostname; + +// EMONCMS SERVER strings +String emoncms_server; +String emoncms_node; +String emoncms_apikey; +String emoncms_fingerprint; + +// MQTT Settings +String mqtt_server; +uint32_t mqtt_port; +String mqtt_topic; +String mqtt_user; +String mqtt_pass; +String mqtt_solar; +String mqtt_grid_ie; +String mqtt_vrms; +String mqtt_announce_topic; + +// 24-bits of Flags +uint32_t flags; + +// Ohm Connect Settings +String ohm; + +// Divert settings +double divert_attack_smoothing_factor; +double divert_decay_smoothing_factor; +uint32_t divert_min_charge_time; + +String esp_hostname_default = "openevse-"+ESPAL.getShortId(); + +void config_changed(String name); + +ConfigOptDefenition flagsOpt = ConfigOptDefenition(flags, 0, "flags", "f"); + +ConfigOpt *opts[] = +{ +// Wifi Network Strings + new ConfigOptDefenition(esid, "", "ssid", "ws"), + new ConfigOptSecret(epass, "", "pass", "wp"), + +// Web server authentication (leave blank for none) + new ConfigOptDefenition(www_username, "", "www_username", "au"), + new ConfigOptSecret(www_password, "", "www_password", "ap"), + +// Advanced settings + new ConfigOptDefenition(esp_hostname, esp_hostname_default, "hostname", "hn"), + +// EMONCMS SERVER strings + new ConfigOptDefenition(emoncms_server, "https://data.openevse.com/emoncms", "emoncms_server", "es"), + new ConfigOptDefenition(emoncms_node, esp_hostname, "emoncms_node", "en"), + new ConfigOptSecret(emoncms_apikey, "", "emoncms_apikey", "ea"), + new ConfigOptDefenition(emoncms_fingerprint, "", "emoncms_fingerprint", "ef"), + +// MQTT Settings + new ConfigOptDefenition(mqtt_server, "emonpi", "mqtt_server", "ms"), + new ConfigOptDefenition(mqtt_port, 1883, "mqtt_port", "mpt"), + new ConfigOptDefenition(mqtt_topic, esp_hostname, "mqtt_topic", "mt"), + new ConfigOptDefenition(mqtt_user, "emonpi", "mqtt_user", "mu"), + new ConfigOptSecret(mqtt_pass, "emonpimqtt2016", "mqtt_pass", "mp"), + new ConfigOptDefenition(mqtt_solar, "", "mqtt_solar", "mo"), + new ConfigOptDefenition(mqtt_grid_ie, "emon/emonpi/power1", "mqtt_grid_ie", "mg"), + new ConfigOptDefenition(mqtt_vrms, "emon/emonpi/vrms", "mqtt_vrms", "mv"), + new ConfigOptDefenition(mqtt_announce_topic, "openevse/announce/"+ESPAL.getShortId(), "mqtt_announce_topic", "ma"), + +// Ohm Connect Settings + new ConfigOptDefenition(ohm, "", "ohm", "o"), + +// Divert settings + new ConfigOptDefenition(divert_attack_smoothing_factor, 0.4, "divert_attack_smoothing_factor", "da"), + new ConfigOptDefenition(divert_decay_smoothing_factor, 0.05, "divert_decay_smoothing_factor", "dd"), + new ConfigOptDefenition(divert_min_charge_time, (10 * 60), "divert_min_charge_time", "dt"), + +// Flags + &flagsOpt, + +// Virtual Options + new ConfigOptVirtualBool(flagsOpt, CONFIG_SERVICE_EMONCMS, CONFIG_SERVICE_EMONCMS, "emoncms_enabled", "ee"), + new ConfigOptVirtualBool(flagsOpt, CONFIG_SERVICE_MQTT, CONFIG_SERVICE_MQTT, "mqtt_enabled", "me"), + new ConfigOptVirtualBool(flagsOpt, CONFIG_SERVICE_OHM, CONFIG_SERVICE_OHM, "ohm_enabled", "oe"), + new ConfigOptVirtualBool(flagsOpt, CONFIG_SERVICE_DIVERT, CONFIG_SERVICE_DIVERT, "divert_enabled", "de"), + new ConfigOptVirtualChargeMode(flagsOpt, "charge_mode", "chmd") +}; + +ConfigJson config(opts, sizeof(opts) / sizeof(opts[0]), EEPROM_SIZE); + +// ------------------------------------------------------------------- +// Reset EEPROM, wipes all settings +// ------------------------------------------------------------------- +void +ResetEEPROM() { + EEPROM.begin(EEPROM_SIZE); + + //DEBUG.println("Erasing EEPROM"); + for (int i = 0; i < EEPROM_SIZE; ++i) { + EEPROM.write(i, 0xff); + //DEBUG.print("#"); + } + EEPROM.end(); +} + +// ------------------------------------------------------------------- +// Load saved settings from EEPROM +// ------------------------------------------------------------------- +void +config_load_settings() +{ + config.onChanged(config_changed); + + if(!config.load()) { + DBUGF("No JSON config found, trying v1 settings"); + config_load_v1_settings(); + } +} + +void config_changed(String name) +{ + DBUGF("%s changed", name.c_str()); + + if(name == "flags") { + divertmode_update(config_divert_enabled() ? DIVERT_MODE_ECO : DIVERT_MODE_NORMAL); + if(mqtt_connected() != config_mqtt_enabled()) { + mqtt_restart(); + } + if(emoncms_connected != config_emoncms_enabled()) { + emoncms_updated = true; + } + } else if(name.startsWith("mqtt_")) { + mqtt_restart(); + } else if(name.startsWith("emoncms_")) { + emoncms_updated = true; + } else if(name == "divert_enabled" || name == "charge_mode") { + DBUGVAR(config_divert_enabled()); + DBUGVAR(config_charge_mode()); + divertmode_update((config_divert_enabled() && 1 == config_charge_mode()) ? DIVERT_MODE_ECO : DIVERT_MODE_NORMAL); + } +} + +void config_commit() +{ + config.commit(); +} + +bool config_deserialize(String& json) { + return config.deserialize(json.c_str()); +} + +bool config_deserialize(const char *json) +{ + return config.deserialize(json); +} + +bool config_deserialize(DynamicJsonDocument &doc) +{ + return config.deserialize(doc); +} + +bool config_serialize(String& json, bool longNames, bool compactOutput, bool hideSecrets) +{ + return config.serialize(json, longNames, compactOutput, hideSecrets); +} + +bool config_serialize(DynamicJsonDocument &doc, bool longNames, bool compactOutput, bool hideSecrets) +{ + return config.serialize(doc, longNames, compactOutput, hideSecrets); +} + +void config_set(const char *name, uint32_t val) { + config.set(name, val); +} +void config_set(const char *name, String val) { + config.set(name, val); +} +void config_set(const char *name, bool val) { + config.set(name, val); +} +void config_set(const char *name, double val) { + config.set(name, val); +} + +void config_save_emoncms(bool enable, String server, String node, String apikey, + String fingerprint) +{ + uint32_t newflags = flags & ~CONFIG_SERVICE_EMONCMS; + if(enable) { + newflags |= CONFIG_SERVICE_EMONCMS; + } + + config.set("emoncms_server", server); + config.set("emoncms_node", node); + config.set("emoncms_apikey", apikey); + config.set("emoncms_fingerprint", fingerprint); + config.set("flags", newflags); + config.commit(); +} + +void +config_save_mqtt(bool enable, String server, uint16_t port, String topic, String user, String pass, String solar, String grid_ie) +{ uint32_t newflags = flags & ~CONFIG_SERVICE_MQTT; + if(enable) { + newflags |= CONFIG_SERVICE_MQTT; + } + + config.set("mqtt_server", server); + config.set("mqtt_port", port); + config.set("mqtt_topic", topic); + config.set("mqtt_user", user); + config.set("mqtt_pass", pass); + config.set("mqtt_solar", solar); + config.set("mqtt_grid_ie", grid_ie); + config.set("flags", newflags); + config.commit(); +} + +void +config_save_admin(String user, String pass) { + config.set("www_username", user); + config.set("www_password", pass); + config.commit(); +} + +void +config_save_advanced(String hostname) { + config.set("hostname", hostname); + config.commit(); +} + +void +config_save_wifi(String qsid, String qpass) +{ + config.set("ssid", qsid); + config.set("pass", qpass); + config.commit(); +} + +void +config_save_ohm(bool enable, String qohm) +{ + uint32_t newflags = flags & ~CONFIG_SERVICE_OHM; + if(enable) { + newflags |= CONFIG_SERVICE_OHM; + } + + config.set("ohm", qohm); + config.set("flags", newflags); + config.commit(); +} + +void +config_save_flags(uint32_t newFlags) { + config.set("flags", newFlags); + config.commit(); +} + +void +config_reset() { + ResetEEPROM(); + config.reset(); +} diff --git a/src/config.h b/src/app_config.h similarity index 70% rename from src/config.h rename to src/app_config.h index aa13349..859d08b 100644 --- a/src/config.h +++ b/src/app_config.h @@ -2,6 +2,7 @@ #define _EMONESP_CONFIG_H #include +#include // ------------------------------------------------------------------- // Load and save the OpenEVSE WiFi config. @@ -21,6 +22,7 @@ extern String www_password; // Advanced settings extern String esp_hostname; +extern String esp_hostname_default; // EMONCMS SERVER strings extern String emoncms_server; @@ -30,11 +32,19 @@ extern String emoncms_fingerprint; // MQTT Settings extern String mqtt_server; +extern uint32_t mqtt_port; extern String mqtt_topic; extern String mqtt_user; extern String mqtt_pass; extern String mqtt_solar; extern String mqtt_grid_ie; +extern String mqtt_vrms; +extern String mqtt_announce_topic; + +// Divert settings +extern double divert_attack_smoothing_factor; +extern double divert_decay_smoothing_factor; +extern uint32_t divert_min_charge_time; // 24-bits of Flags extern uint32_t flags; @@ -42,6 +52,8 @@ extern uint32_t flags; #define CONFIG_SERVICE_EMONCMS (1 << 0) #define CONFIG_SERVICE_MQTT (1 << 1) #define CONFIG_SERVICE_OHM (1 << 2) +#define CONFIG_SERVICE_DIVERT (1 << 9) +#define CONFIG_CHARGE_MODE (7 << 10) // 3 bits for mode inline bool config_emoncms_enabled() { return CONFIG_SERVICE_EMONCMS == (flags & CONFIG_SERVICE_EMONCMS); @@ -55,6 +67,14 @@ inline bool config_ohm_enabled() { return CONFIG_SERVICE_OHM == (flags & CONFIG_SERVICE_OHM); } +inline bool config_divert_enabled() { + return CONFIG_SERVICE_DIVERT == (flags & CONFIG_SERVICE_DIVERT); +} + +inline uint8_t config_charge_mode() { + return (flags & CONFIG_CHARGE_MODE) >> 10; +} + // Ohm Connect Settings extern String ohm; @@ -62,6 +82,7 @@ extern String ohm; // Load saved settings // ------------------------------------------------------------------- extern void config_load_settings(); +extern void config_load_v1_settings(); // ------------------------------------------------------------------- // Save the EmonCMS server details @@ -71,7 +92,7 @@ extern void config_save_emoncms(bool enable, String server, String node, String // ------------------------------------------------------------------- // Save the MQTT broker details // ------------------------------------------------------------------- -extern void config_save_mqtt(bool enable, String server, String topic, String user, String pass, String solar, String grid_ie); +extern void config_save_mqtt(bool enable, String server, uint16_t port, String topic, String user, String pass, String solar, String grid_ie); // ------------------------------------------------------------------- // Save the admin/web interface details @@ -103,4 +124,19 @@ extern void config_save_flags(uint32_t flags); // ------------------------------------------------------------------- extern void config_reset(); +void config_set(const char *name, uint32_t val); +void config_set(const char *name, String val); +void config_set(const char *name, bool val); +void config_set(const char *name, double val); + +// Read config settings from JSON object +bool config_deserialize(String& json); +bool config_deserialize(const char *json); +bool config_deserialize(DynamicJsonDocument &doc); +void config_commit(); + +// Write config settings to JSON object +bool config_serialize(String& json, bool longNames = true, bool compactOutput = false, bool hideSecrets = false); +bool config_serialize(DynamicJsonDocument &doc, bool longNames = true, bool compactOutput = false, bool hideSecrets = false); + #endif // _EMONESP_CONFIG_H diff --git a/src/app_config_mode.h b/src/app_config_mode.h new file mode 100644 index 0000000..de77405 --- /dev/null +++ b/src/app_config_mode.h @@ -0,0 +1,59 @@ +#ifndef app_config_mode_h +#define app_config_mode_h + +#include +#include + +#include "mqtt.h" +#include "app_config.h" + +class ConfigOptVirtualChargeMode : public ConfigOpt +{ +protected: + ConfigOptDefenition &_base; + +public: + ConfigOptVirtualChargeMode(ConfigOptDefenition &b, const char *l, const char *s) : + ConfigOpt(l, s), + _base(b) + { + } + + String get() { + int mode = (_base.get() & CONFIG_CHARGE_MODE) >> 10; + return 0 == mode ? "fast" : "eco"; + } + + virtual bool set(String value) { + DBUGF("Set charge mode to %s", value.c_str()); + uint32_t newVal = _base.get() & ~CONFIG_CHARGE_MODE; + if(value == "eco") { + newVal |= 1 << 10; + } + return _base.set(newVal); + } + + virtual bool serialize(DynamicJsonDocument &doc, bool longNames, bool compactOutput, bool hideSecrets) { + if(!compactOutput) { + doc[name(longNames)] = get(); + return true; + } + + return false; + } + + virtual bool deserialize(DynamicJsonDocument &doc) { + if(doc.containsKey(_long)) { + return set(doc[_long].as()); + } else if(doc.containsKey(_short)) { \ + return set(doc[_short].as()); + } + + return false; + } + + virtual void setDefault() { + } +}; + +#endif diff --git a/src/app_config_v1.cpp b/src/app_config_v1.cpp new file mode 100644 index 0000000..62a7143 --- /dev/null +++ b/src/app_config_v1.cpp @@ -0,0 +1,169 @@ +#include "emonesp.h" +#include "app_config.h" +#include "espal.h" + +#include +#include // Save config settings + +#define EEPROM_ESID_SIZE 32 +#define EEPROM_EPASS_SIZE 64 +#define EEPROM_EMON_API_KEY_SIZE 33 +#define EEPROM_EMON_SERVER_SIZE 45 +#define EEPROM_EMON_NODE_SIZE 32 +#define EEPROM_MQTT_SERVER_SIZE 45 +#define EEPROM_MQTT_TOPIC_SIZE 32 +#define EEPROM_MQTT_USER_SIZE 32 +#define EEPROM_MQTT_PASS_SIZE 64 +#define EEPROM_MQTT_SOLAR_SIZE 30 +#define EEPROM_MQTT_GRID_IE_SIZE 30 +#define EEPROM_EMON_FINGERPRINT_SIZE 60 +#define EEPROM_WWW_USER_SIZE 15 +#define EEPROM_WWW_PASS_SIZE 15 +#define EEPROM_OHM_KEY_SIZE 10 +#define EEPROM_FLAGS_SIZE 4 +#define EEPROM_HOSTNAME_SIZE 32 +#define EEPROM_SIZE 1024 + +#define EEPROM_ESID_START 0 +#define EEPROM_ESID_END (EEPROM_ESID_START + EEPROM_ESID_SIZE) +#define EEPROM_EPASS_START EEPROM_ESID_END +#define EEPROM_EPASS_END (EEPROM_EPASS_START + EEPROM_EPASS_SIZE) +#define EEPROM_EMON_SERVER_START EEPROM_EPASS_END + 32 /* EEPROM_EMON_API_KEY used to be stored before this */ +#define EEPROM_EMON_SERVER_END (EEPROM_EMON_SERVER_START + EEPROM_EMON_SERVER_SIZE) +#define EEPROM_EMON_NODE_START EEPROM_EMON_SERVER_END +#define EEPROM_EMON_NODE_END (EEPROM_EMON_NODE_START + EEPROM_EMON_NODE_SIZE) +#define EEPROM_MQTT_SERVER_START EEPROM_EMON_NODE_END +#define EEPROM_MQTT_SERVER_END (EEPROM_MQTT_SERVER_START + EEPROM_MQTT_SERVER_SIZE) +#define EEPROM_MQTT_TOPIC_START EEPROM_MQTT_SERVER_END +#define EEPROM_MQTT_TOPIC_END (EEPROM_MQTT_TOPIC_START + EEPROM_MQTT_TOPIC_SIZE) +#define EEPROM_MQTT_USER_START EEPROM_MQTT_TOPIC_END +#define EEPROM_MQTT_USER_END (EEPROM_MQTT_USER_START + EEPROM_MQTT_USER_SIZE) +#define EEPROM_MQTT_PASS_START EEPROM_MQTT_USER_END +#define EEPROM_MQTT_PASS_END (EEPROM_MQTT_PASS_START + EEPROM_MQTT_PASS_SIZE) +#define EEPROM_MQTT_SOLAR_START EEPROM_MQTT_PASS_END +#define EEPROM_MQTT_SOLAR_END (EEPROM_MQTT_SOLAR_START + EEPROM_MQTT_SOLAR_SIZE) +#define EEPROM_MQTT_GRID_IE_START EEPROM_MQTT_SOLAR_END +#define EEPROM_MQTT_GRID_IE_END (EEPROM_MQTT_GRID_IE_START + EEPROM_MQTT_GRID_IE_SIZE) +#define EEPROM_EMON_FINGERPRINT_START EEPROM_MQTT_GRID_IE_END +#define EEPROM_EMON_FINGERPRINT_END (EEPROM_EMON_FINGERPRINT_START + EEPROM_EMON_FINGERPRINT_SIZE) +#define EEPROM_WWW_USER_START EEPROM_EMON_FINGERPRINT_END +#define EEPROM_WWW_USER_END (EEPROM_WWW_USER_START + EEPROM_WWW_USER_SIZE) +#define EEPROM_WWW_PASS_START EEPROM_WWW_USER_END +#define EEPROM_WWW_PASS_END (EEPROM_WWW_PASS_START + EEPROM_WWW_PASS_SIZE) +#define EEPROM_OHM_KEY_START EEPROM_WWW_PASS_END +#define EEPROM_OHM_KEY_END (EEPROM_OHM_KEY_START + EEPROM_OHM_KEY_SIZE) +#define EEPROM_FLAGS_START EEPROM_OHM_KEY_END +#define EEPROM_FLAGS_END (EEPROM_FLAGS_START + EEPROM_FLAGS_SIZE) +#define EEPROM_EMON_API_KEY_START EEPROM_FLAGS_END +#define EEPROM_EMON_API_KEY_END (EEPROM_EMON_API_KEY_START + EEPROM_EMON_API_KEY_SIZE) +#define EEPROM_HOSTNAME_START EEPROM_EMON_API_KEY_END +#define EEPROM_HOSTNAME_END (EEPROM_HOSTNAME_START + EEPROM_HOSTNAME_SIZE) +#define EEPROM_CONFIG_END EEPROM_HOSTNAME_END + +#if EEPROM_CONFIG_END > EEPROM_SIZE +#error EEPROM_SIZE too small +#endif + +#define CHECKSUM_SEED 128 + +bool +EEPROM_read_string(int start, int count, String & val) { + String newVal; + byte checksum = CHECKSUM_SEED; + for (int i = 0; i < count - 1; ++i) { + byte c = EEPROM.read(start + i); + if (c != 0 && c != 255) { + checksum ^= c; + newVal += (char) c; + } else { + break; + } + } + + // Check the checksum + byte c = EEPROM.read(start + (count - 1)); + DBUGF("Got '%s' %d == %d @ %d:%d", newVal.c_str(), c, checksum, start, count); + if(c == checksum) { + val = newVal; + return true; + } + + return false; +} + +void +EEPROM_read_uint24(int start, uint32_t & val) { + byte checksum = CHECKSUM_SEED; + uint32_t newVal = 0; + for (int i = 0; i < 3; ++i) { + byte c = EEPROM.read(start + i); + checksum ^= c; + newVal = (newVal << 8) | c; + } + + // Check the checksum + byte c = EEPROM.read(start + 3); + DBUGF("Got '%06x' %d == %d @ %d:4", newVal, c, checksum, start); + if(c == checksum) { + val = newVal; + } +} + +// ------------------------------------------------------------------- +// Load saved settings from EEPROM +// ------------------------------------------------------------------- +void +config_load_v1_settings() { + DBUGLN("Loading config"); + + EEPROM.begin(EEPROM_SIZE); + + // Device Hostname, needs to be read first as other config defaults depend on it + EEPROM_read_string(EEPROM_HOSTNAME_START, EEPROM_HOSTNAME_SIZE, + esp_hostname); + + // Load WiFi values + EEPROM_read_string(EEPROM_ESID_START, EEPROM_ESID_SIZE, esid); + EEPROM_read_string(EEPROM_EPASS_START, EEPROM_EPASS_SIZE, epass); + + // EmonCMS settings + EEPROM_read_string(EEPROM_EMON_API_KEY_START, EEPROM_EMON_API_KEY_SIZE, + emoncms_apikey); + EEPROM_read_string(EEPROM_EMON_SERVER_START, EEPROM_EMON_SERVER_SIZE, + emoncms_server); + EEPROM_read_string(EEPROM_EMON_NODE_START, EEPROM_EMON_NODE_SIZE, + emoncms_node); + EEPROM_read_string(EEPROM_EMON_FINGERPRINT_START, EEPROM_EMON_FINGERPRINT_SIZE, + emoncms_fingerprint); + + // MQTT settings + EEPROM_read_string(EEPROM_MQTT_SERVER_START, EEPROM_MQTT_SERVER_SIZE, + mqtt_server); + EEPROM_read_string(EEPROM_MQTT_TOPIC_START, EEPROM_MQTT_TOPIC_SIZE, + mqtt_topic); + EEPROM_read_string(EEPROM_MQTT_USER_START, EEPROM_MQTT_USER_SIZE, + mqtt_user); + EEPROM_read_string(EEPROM_MQTT_PASS_START, EEPROM_MQTT_PASS_SIZE, + mqtt_pass); + EEPROM_read_string(EEPROM_MQTT_SOLAR_START, EEPROM_MQTT_SOLAR_SIZE, + mqtt_solar); + EEPROM_read_string(EEPROM_MQTT_GRID_IE_START, EEPROM_MQTT_GRID_IE_SIZE, + mqtt_grid_ie); + + // Web server credentials + EEPROM_read_string(EEPROM_WWW_USER_START, EEPROM_WWW_USER_SIZE, + www_username); + EEPROM_read_string(EEPROM_WWW_PASS_START, EEPROM_WWW_PASS_SIZE, + www_password); + // Web server credentials + EEPROM_read_string(EEPROM_HOSTNAME_START, EEPROM_HOSTNAME_SIZE, + esp_hostname); + + // Ohm Connect Settings + EEPROM_read_string(EEPROM_OHM_KEY_START, EEPROM_OHM_KEY_SIZE, ohm); + + // Flags + EEPROM_read_uint24(EEPROM_FLAGS_START, flags); + + EEPROM.end(); +} diff --git a/src/config.cpp b/src/config.cpp deleted file mode 100644 index d3037c6..0000000 --- a/src/config.cpp +++ /dev/null @@ -1,383 +0,0 @@ -#include "emonesp.h" -#include "config.h" - -#include -#include // Save config settings - -// Wifi Network Strings -String esid = ""; -String epass = ""; - -// Web server authentication (leave blank for none) -String www_username = ""; -String www_password = ""; - -// Advanced settings -String esp_hostname = ""; - -// EMONCMS SERVER strings -String emoncms_server = ""; -String emoncms_node = ""; -String emoncms_apikey = ""; -String emoncms_fingerprint = ""; - -// MQTT Settings -String mqtt_server = ""; -String mqtt_topic = ""; -String mqtt_user = ""; -String mqtt_pass = ""; -String mqtt_solar = ""; -String mqtt_grid_ie = ""; - -// Ohm Connect Settings -String ohm = ""; - -// Flags -uint32_t flags; - -#define EEPROM_ESID_SIZE 32 -#define EEPROM_EPASS_SIZE 64 -#define EEPROM_EMON_API_KEY_SIZE 33 -#define EEPROM_EMON_SERVER_SIZE 45 -#define EEPROM_EMON_NODE_SIZE 32 -#define EEPROM_MQTT_SERVER_SIZE 45 -#define EEPROM_MQTT_TOPIC_SIZE 32 -#define EEPROM_MQTT_USER_SIZE 32 -#define EEPROM_MQTT_PASS_SIZE 64 -#define EEPROM_MQTT_SOLAR_SIZE 30 -#define EEPROM_MQTT_GRID_IE_SIZE 30 -#define EEPROM_EMON_FINGERPRINT_SIZE 60 -#define EEPROM_WWW_USER_SIZE 15 -#define EEPROM_WWW_PASS_SIZE 15 -#define EEPROM_OHM_KEY_SIZE 10 -#define EEPROM_FLAGS_SIZE 4 -#define EEPROM_HOSTNAME_SIZE 32 -#define EEPROM_SIZE 1024 - -#define EEPROM_ESID_START 0 -#define EEPROM_ESID_END (EEPROM_ESID_START + EEPROM_ESID_SIZE) -#define EEPROM_EPASS_START EEPROM_ESID_END -#define EEPROM_EPASS_END (EEPROM_EPASS_START + EEPROM_EPASS_SIZE) -#define EEPROM_EMON_SERVER_START EEPROM_EPASS_END + 32 /* EEPROM_EMON_API_KEY used to be stored before this */ -#define EEPROM_EMON_SERVER_END (EEPROM_EMON_SERVER_START + EEPROM_EMON_SERVER_SIZE) -#define EEPROM_EMON_NODE_START EEPROM_EMON_SERVER_END -#define EEPROM_EMON_NODE_END (EEPROM_EMON_NODE_START + EEPROM_EMON_NODE_SIZE) -#define EEPROM_MQTT_SERVER_START EEPROM_EMON_NODE_END -#define EEPROM_MQTT_SERVER_END (EEPROM_MQTT_SERVER_START + EEPROM_MQTT_SERVER_SIZE) -#define EEPROM_MQTT_TOPIC_START EEPROM_MQTT_SERVER_END -#define EEPROM_MQTT_TOPIC_END (EEPROM_MQTT_TOPIC_START + EEPROM_MQTT_TOPIC_SIZE) -#define EEPROM_MQTT_USER_START EEPROM_MQTT_TOPIC_END -#define EEPROM_MQTT_USER_END (EEPROM_MQTT_USER_START + EEPROM_MQTT_USER_SIZE) -#define EEPROM_MQTT_PASS_START EEPROM_MQTT_USER_END -#define EEPROM_MQTT_PASS_END (EEPROM_MQTT_PASS_START + EEPROM_MQTT_PASS_SIZE) -#define EEPROM_MQTT_SOLAR_START EEPROM_MQTT_PASS_END -#define EEPROM_MQTT_SOLAR_END (EEPROM_MQTT_SOLAR_START + EEPROM_MQTT_SOLAR_SIZE) -#define EEPROM_MQTT_GRID_IE_START EEPROM_MQTT_SOLAR_END -#define EEPROM_MQTT_GRID_IE_END (EEPROM_MQTT_GRID_IE_START + EEPROM_MQTT_GRID_IE_SIZE) -#define EEPROM_EMON_FINGERPRINT_START EEPROM_MQTT_GRID_IE_END -#define EEPROM_EMON_FINGERPRINT_END (EEPROM_EMON_FINGERPRINT_START + EEPROM_EMON_FINGERPRINT_SIZE) -#define EEPROM_WWW_USER_START EEPROM_EMON_FINGERPRINT_END -#define EEPROM_WWW_USER_END (EEPROM_WWW_USER_START + EEPROM_WWW_USER_SIZE) -#define EEPROM_WWW_PASS_START EEPROM_WWW_USER_END -#define EEPROM_WWW_PASS_END (EEPROM_WWW_PASS_START + EEPROM_WWW_PASS_SIZE) -#define EEPROM_OHM_KEY_START EEPROM_WWW_PASS_END -#define EEPROM_OHM_KEY_END (EEPROM_OHM_KEY_START + EEPROM_OHM_KEY_SIZE) -#define EEPROM_FLAGS_START EEPROM_OHM_KEY_END -#define EEPROM_FLAGS_END (EEPROM_FLAGS_START + EEPROM_FLAGS_SIZE) -#define EEPROM_EMON_API_KEY_START EEPROM_FLAGS_END -#define EEPROM_EMON_API_KEY_END (EEPROM_EMON_API_KEY_START + EEPROM_EMON_API_KEY_SIZE) -#define EEPROM_HOSTNAME_START EEPROM_EMON_API_KEY_END -#define EEPROM_HOSTNAME_END (EEPROM_HOSTNAME_START + EEPROM_HOSTNAME_SIZE) -#define EEPROM_CONFIG_END EEPROM_HOSTNAME_END - -#if EEPROM_CONFIG_END > EEPROM_SIZE -#error EEPROM_SIZE too small -#endif - -#define CHECKSUM_SEED 128 - -// ------------------------------------------------------------------- -// Reset EEPROM, wipes all settings -// ------------------------------------------------------------------- -void -ResetEEPROM() { - EEPROM.begin(EEPROM_SIZE); - - //DEBUG.println("Erasing EEPROM"); - for (int i = 0; i < EEPROM_SIZE; ++i) { - EEPROM.write(i, 0xff); - //DEBUG.print("#"); - } - EEPROM.end(); -} - -void -EEPROM_read_string(int start, int count, String & val, String defaultVal = "") { - byte checksum = CHECKSUM_SEED; - for (int i = 0; i < count - 1; ++i) { - byte c = EEPROM.read(start + i); - if (c != 0 && c != 255) { - checksum ^= c; - val += (char) c; - } else { - break; - } - } - - // Check the checksum - byte c = EEPROM.read(start + (count - 1)); - DBUGF("Got '%s' %d == %d @ %d:%d", val.c_str(), c, checksum, start, count); - if(c != checksum) { - DBUGF("Using default '%s'", defaultVal.c_str()); - val = defaultVal; - } -} - -void -EEPROM_write_string(int start, int count, String val) { - byte checksum = CHECKSUM_SEED; - for (int i = 0; i < count - 1; ++i) { - if (i < val.length()) { - checksum ^= val[i]; - EEPROM.write(start + i, val[i]); - } else { - EEPROM.write(start + i, 0); - } - } - EEPROM.write(start + (count - 1), checksum); - DBUGF("Saved '%s' %d @ %d:%d", val.c_str(), checksum, start, count); -} - -void -EEPROM_read_uint24(int start, uint32_t & val, uint32_t defaultVal = 0) { - byte checksum = CHECKSUM_SEED; - val = 0; - for (int i = 0; i < 3; ++i) { - byte c = EEPROM.read(start + i); - checksum ^= c; - val = (val << 8) | c; - } - - // Check the checksum - byte c = EEPROM.read(start + 3); - DBUGF("Got '%06x' %d == %d @ %d:4", val, c, checksum, start); - if(c != checksum) { - DBUGF("Using default '%06x'", defaultVal); - val = defaultVal; - } -} - -void -EEPROM_write_uint24(int start, uint32_t value) { - byte checksum = CHECKSUM_SEED; - uint32_t val = value; - for (int i = 2; i >= 0; --i) { - byte c = val & 0xff; - val = val >> 8; - checksum ^= c; - EEPROM.write(start + i, c); - } - EEPROM.write(start + 3, checksum); - DBUGF("Saved '%06x' %d @ %d:4", value, checksum, start); -} - -// ------------------------------------------------------------------- -// Load saved settings from EEPROM -// ------------------------------------------------------------------- -void -config_load_settings() { - DBUGLN("Loading config"); - EEPROM.begin(EEPROM_SIZE); - - // Load WiFi values - EEPROM_read_string(EEPROM_ESID_START, EEPROM_ESID_SIZE, esid); - EEPROM_read_string(EEPROM_EPASS_START, EEPROM_EPASS_SIZE, epass); - - // EmonCMS settings - EEPROM_read_string(EEPROM_EMON_API_KEY_START, EEPROM_EMON_API_KEY_SIZE, - emoncms_apikey); - EEPROM_read_string(EEPROM_EMON_SERVER_START, EEPROM_EMON_SERVER_SIZE, - emoncms_server, "data.openevse.com/emoncms"); - EEPROM_read_string(EEPROM_EMON_NODE_START, EEPROM_EMON_NODE_SIZE, - emoncms_node, "openevse"); - EEPROM_read_string(EEPROM_EMON_FINGERPRINT_START, EEPROM_EMON_FINGERPRINT_SIZE, - emoncms_fingerprint,""); - - // MQTT settings - EEPROM_read_string(EEPROM_MQTT_SERVER_START, EEPROM_MQTT_SERVER_SIZE, - mqtt_server, "emonpi"); - EEPROM_read_string(EEPROM_MQTT_TOPIC_START, EEPROM_MQTT_TOPIC_SIZE, - mqtt_topic, "openevse"); - EEPROM_read_string(EEPROM_MQTT_USER_START, EEPROM_MQTT_USER_SIZE, - mqtt_user, "emonpi"); - EEPROM_read_string(EEPROM_MQTT_PASS_START, EEPROM_MQTT_PASS_SIZE, - mqtt_pass, "emonpimqtt2016"); - EEPROM_read_string(EEPROM_MQTT_SOLAR_START, EEPROM_MQTT_SOLAR_SIZE, - mqtt_solar); - EEPROM_read_string(EEPROM_MQTT_GRID_IE_START, EEPROM_MQTT_GRID_IE_SIZE, - mqtt_grid_ie, "emon/emonpi/power1"); - - // Web server credentials - EEPROM_read_string(EEPROM_WWW_USER_START, EEPROM_WWW_USER_SIZE, - www_username, ""); - EEPROM_read_string(EEPROM_WWW_PASS_START, EEPROM_WWW_PASS_SIZE, - www_password, ""); - - // Web server credentials - EEPROM_read_string(EEPROM_HOSTNAME_START, EEPROM_HOSTNAME_SIZE, - esp_hostname, "openevse"); - - // Ohm Connect Settings - EEPROM_read_string(EEPROM_OHM_KEY_START, EEPROM_OHM_KEY_SIZE, ohm); - - // Flags - EEPROM_read_uint24(EEPROM_FLAGS_START, flags, 0); - - EEPROM.end(); -} - -void -config_save_emoncms(bool enable, String server, String node, String apikey, - String fingerprint) -{ - EEPROM.begin(EEPROM_SIZE); - - flags = flags & ~CONFIG_SERVICE_EMONCMS; - if(enable) { - flags |= CONFIG_SERVICE_EMONCMS; - } - - emoncms_server = server; - emoncms_node = node; - emoncms_apikey = apikey; - emoncms_fingerprint = fingerprint; - - // save apikey to EEPROM - EEPROM_write_string(EEPROM_EMON_API_KEY_START, EEPROM_EMON_API_KEY_SIZE, - emoncms_apikey); - - // save emoncms server to EEPROM max 45 characters - EEPROM_write_string(EEPROM_EMON_SERVER_START, EEPROM_EMON_SERVER_SIZE, - emoncms_server); - - // save emoncms node to EEPROM max 32 characters - EEPROM_write_string(EEPROM_EMON_NODE_START, EEPROM_EMON_NODE_SIZE, - emoncms_node); - - // save emoncms HTTPS fingerprint to EEPROM max 60 characters - EEPROM_write_string(EEPROM_EMON_FINGERPRINT_START, - EEPROM_EMON_FINGERPRINT_SIZE, emoncms_fingerprint); - - EEPROM_write_uint24(EEPROM_FLAGS_START, flags); - - EEPROM.end(); -} - -void -config_save_mqtt(bool enable, String server, String topic, String user, String pass, String solar, String grid_ie) -{ - EEPROM.begin(EEPROM_SIZE); - - flags = flags & ~CONFIG_SERVICE_MQTT; - if(enable) { - flags |= CONFIG_SERVICE_MQTT; - } - - mqtt_server = server; - mqtt_topic = topic; - mqtt_user = user; - mqtt_pass = pass; - mqtt_solar = solar; - mqtt_grid_ie = grid_ie; - - EEPROM_write_string(EEPROM_MQTT_SERVER_START, EEPROM_MQTT_SERVER_SIZE, - mqtt_server); - EEPROM_write_string(EEPROM_MQTT_TOPIC_START, EEPROM_MQTT_TOPIC_SIZE, - mqtt_topic); - EEPROM_write_string(EEPROM_MQTT_USER_START, EEPROM_MQTT_USER_SIZE, - mqtt_user); - EEPROM_write_string(EEPROM_MQTT_PASS_START, EEPROM_MQTT_PASS_SIZE, - mqtt_pass); - EEPROM_write_string(EEPROM_MQTT_SOLAR_START, EEPROM_MQTT_SOLAR_SIZE, mqtt_solar); - EEPROM_write_string(EEPROM_MQTT_GRID_IE_START, EEPROM_MQTT_GRID_IE_SIZE, mqtt_grid_ie); - - EEPROM_write_uint24(EEPROM_FLAGS_START, flags); - - EEPROM.end(); -} - -void -config_save_admin(String user, String pass) { - EEPROM.begin(EEPROM_SIZE); - - www_username = user; - www_password = pass; - - EEPROM_write_string(EEPROM_WWW_USER_START, EEPROM_WWW_USER_SIZE, user); - EEPROM_write_string(EEPROM_WWW_PASS_START, EEPROM_WWW_PASS_SIZE, pass); - - EEPROM.end(); -} - -void -config_save_advanced(String host) { - EEPROM.begin(EEPROM_SIZE); - - esp_hostname = host; - - EEPROM_write_string(EEPROM_HOSTNAME_START, EEPROM_HOSTNAME_SIZE, host); - - EEPROM.end(); -} - -void -config_save_wifi(String qsid, String qpass) -{ - EEPROM.begin(EEPROM_SIZE); - - esid = qsid; - epass = qpass; - - EEPROM_write_string(EEPROM_ESID_START, EEPROM_ESID_SIZE, qsid); - EEPROM_write_string(EEPROM_EPASS_START, EEPROM_EPASS_SIZE, qpass); - - EEPROM.end(); -} - -void -config_save_ohm(bool enable, String qohm) -{ - EEPROM.begin(EEPROM_SIZE); - - flags = flags & ~CONFIG_SERVICE_OHM; - if(enable) { - flags |= CONFIG_SERVICE_OHM; - } - - ohm = qohm; - - EEPROM_write_string(EEPROM_OHM_KEY_START, EEPROM_OHM_KEY_SIZE, qohm); - - EEPROM_write_uint24(EEPROM_FLAGS_START, flags); - - EEPROM.end(); -} - -void -config_save_flags(uint32_t newFlags) { - if(flags != newFlags) - { - EEPROM.begin(EEPROM_SIZE); - - flags = newFlags; - - EEPROM_write_uint24(EEPROM_FLAGS_START, flags); - - EEPROM.end(); - } -} - -void -config_reset() { - ResetEEPROM(); -} diff --git a/src/debug.h b/src/debug.h index 1de666a..00c121d 100644 --- a/src/debug.h +++ b/src/debug.h @@ -18,12 +18,12 @@ #define DEBUG_BEGIN(speed) DEBUG_PORT.begin(speed) -#ifdef ARDUINO_ESP8266_RELEASE_2_4_0 -// Serial.printf_P needs Git version of Arduino Core -#define DBUGF(format, ...) DEBUG_PORT.printf_P(PSTR(format "\n"), ##__VA_ARGS__) -#else +//#ifdef ARDUINO_ESP8266_RELEASE_2_4_0 +//// Serial.printf_P needs Git version of Arduino Core +//#define DBUGF(format, ...) DEBUG_PORT.printf_P(PSTR(format "\n"), ##__VA_ARGS__) +//#else #define DBUGF(format, ...) DEBUG_PORT.printf(format "\n", ##__VA_ARGS__) -#endif +//#endif #define DBUG(...) DEBUG_PORT.print(__VA_ARGS__) #define DBUGLN(...) DEBUG_PORT.println(__VA_ARGS__) diff --git a/src/divert.cpp b/src/divert.cpp index 4e89285..fbe23d8 100644 --- a/src/divert.cpp +++ b/src/divert.cpp @@ -5,11 +5,14 @@ #include #include "emonesp.h" #include "input.h" -#include "config.h" +#include "app_config.h" #include "RapiSender.h" #include "mqtt.h" #include "event.h" #include "openevse.h" +#include "divert.h" + +#include // 1: Normal / Fast Charge (default): // Charging at maximum rate irrespective of solar PV / grid_ie output @@ -23,11 +26,6 @@ // If EVSE is sleeping charging will not start until solar PV / excess power > min chanrge rate // Once charging begins it will not pause even if solaer PV / excess power drops less then minimm charge rate. This avoids wear on the relay and the car -#define SERVICE_LEVEL2_VOLTAGE 240 - -#define DIVERT_MODE_NORMAL 1 -#define DIVERT_MODE_ECO 2 - #define GRID_IE_RESERVE_POWER 100.0 // Default to normal charging unless set. Divert mode always defaults back to 1 if unit is reset (divertmode not saved in EEPROM) @@ -40,8 +38,24 @@ int charge_rate = 0; int last_state = OPENEVSE_STATE_INVALID; uint32_t lastUpdate = 0; + +double avalible_current = 0; +double smoothed_avalible_current = 0; + +time_t min_charge_end = 0; + +bool divert_active = false; + extern RapiSender rapiSender; +// define as 'weak' so the simulator can override +time_t __attribute__((weak)) divertmode_get_time() +{ + struct timeval now; + gettimeofday(&now, NULL); + return now.tv_sec; +} + // Update divert mode e.g. Normal / Eco // function called when divert mode is changed void divertmode_update(byte newmode) @@ -56,26 +70,38 @@ void divertmode_update(byte newmode) { case DIVERT_MODE_NORMAL: // Restore the max charge current - rapiSender.sendCmd(String(F("$SC ")) + String(max_charge_current)); + rapiSender.sendCmdSync(String(F("$SC ")) + String(max_charge_current)); DBUGF("Restore max I: %d", max_charge_current); break; case DIVERT_MODE_ECO: charge_rate = 0; + avalible_current = 0; + smoothed_avalible_current = 0; + min_charge_end = 0; + // Read the current charge current, assume this is the max set by the user - if(0 == rapiSender.sendCmd(F("$GE"))) { + if(0 == rapiSender.sendCmdSync(F("$GE"))) { max_charge_current = String(rapiSender.getToken(1)).toInt(); DBUGF("Read max I: %d", max_charge_current); } + if(OPENEVSE_STATE_SLEEPING != state) + { + if(0 == rapiSender.sendCmdSync(F("$FS"))) + { + DBUGLN(F("Divert activated, entered sleep mode")); + divert_active = false; + } + } break; default: return; } - String event = F("{\"divertmode\":"); - event += String(divertmode); - event += F("}"); + StaticJsonDocument<128> event; + event["divertmode"] = divertmode; + event["divert_active"] = divert_active; event_send(event); } } @@ -84,19 +110,6 @@ void divert_current_loop() { Profile_Start(divert_current_loop); - if(last_state != state) - { - DBUGVAR(last_state); - DBUGVAR(state); - DBUGVAR(divertmode); - - // Revert to normal mode on disconnecting the car - if(OPENEVSE_STATE_NOT_CONNECTED == state && DIVERT_MODE_ECO == divertmode) { - divertmode_update(DIVERT_MODE_NORMAL); - } - last_state = state; - } - Profile_End(divert_current_loop, 5); } //end divert_current_loop @@ -105,20 +118,26 @@ void divert_update_state() { Profile_Start(divert_update_state); + StaticJsonDocument<128> event; + event["divert_update"] = 0; + + if(mqtt_grid_ie != "") { + event["grid_ie"] = grid_ie; + } else { + event["solar"] = solar; + } + // If divert mode = Eco (2) if (divertmode == DIVERT_MODE_ECO) { - int current_charge_rate; + int current_charge_rate = charge_rate; // Read the current charge rate - if(0 == rapiSender.sendCmd(F("$GE"))) { + if(0 == rapiSender.sendCmdSync(F("$GE"))) { current_charge_rate = String(rapiSender.getToken(1)).toInt(); DBUGVAR(current_charge_rate); } - // IMPROVE: Read from OpenEVSE or emonTX (MQTT) - int voltage = SERVICE_LEVEL2_VOLTAGE; - // Calculate current if (mqtt_grid_ie != "") { @@ -127,11 +146,12 @@ void divert_update_state() // grid_ie is negative when exporting // If grid feeds is available and exporting (negative) - double Igrid_ie = (double)grid_ie / (double)voltage; + DBUGVAR(voltage); + double Igrid_ie = (double)grid_ie / voltage; DBUGVAR(Igrid_ie); // Subtract the current charge the EV is using from the Grid IE - if(0 == rapiSender.sendCmd(F("$GG"))) { + if(0 == rapiSender.sendCmdSync(F("$GG"))) { int milliAmps = String(rapiSender.getToken(1)).toInt(); double amps = (double)milliAmps / 1000.0; DBUGVAR(amps); @@ -142,24 +162,33 @@ void divert_update_state() if (Igrid_ie < 0) { // If excess power - double reserve = GRID_IE_RESERVE_POWER / (double)voltage; + double reserve = GRID_IE_RESERVE_POWER / voltage; DBUGVAR(reserve); - charge_rate = (int)floor(-Igrid_ie - reserve); + avalible_current = (-Igrid_ie - reserve); } else { // no excess, so use the min charge - charge_rate = 0; + avalible_current = 0; } } else if (mqtt_solar!="") { // if grid feed is not available: charge rate = solar generation + DBUGVAR(voltage); + avalible_current = (double)solar / voltage; + } - double Isolar = (double)solar / (double)voltage; - DBUGVAR(Isolar); - charge_rate = (int)floor(Isolar); + if(avalible_current < 0) { + avalible_current = 0; } + DBUGVAR(avalible_current); + + double scale = avalible_current > smoothed_avalible_current ? divert_attack_smoothing_factor : divert_decay_smoothing_factor; + smoothed_avalible_current = (avalible_current * scale) + (smoothed_avalible_current * (1 - scale)); + DBUGVAR(smoothed_avalible_current); + + charge_rate = (int)floor(avalible_current); if(OPENEVSE_STATE_SLEEPING != state) { // If we are not sleeping, make sure we are the minimum current @@ -168,7 +197,7 @@ void divert_update_state() DBUGVAR(charge_rate); - if(charge_rate >= min_charge_current) + if(smoothed_avalible_current >= min_charge_current) { // Cap the charge rate at the configured maximum charge_rate = min(charge_rate, static_cast(max_charge_current)); @@ -179,13 +208,15 @@ void divert_update_state() // Set charge rate via RAPI bool chargeRateSet = false; // Try and set current with new API with volatile flag (don't save the current rate to EEPROM) - if(0 == rapiSender.sendCmd(String(F("$SC ")) + String(charge_rate) + String(F(" V")))) { + if(0 == rapiSender.sendCmdSync(String(F("$SC ")) + String(charge_rate) + String(F(" V")))) { chargeRateSet = true; - } else if(0 == rapiSender.sendCmd(String(F("$SC ")) + String(charge_rate))) { + } else if(0 == rapiSender.sendCmdSync(String(F("$SC ")) + String(charge_rate))) { // Fallback to old API chargeRateSet = true; } - if(chargeRateSet = true) { + + if(true == chargeRateSet) + { DBUGF("Charge rate set to %d", charge_rate); pilot = charge_rate; } @@ -198,7 +229,7 @@ void divert_update_state() bool chargeStarted = false; // Check if the timer is enabled, we need to do a bit of hackery if it is - if(0 == rapiSender.sendCmd("$GD")) + if(0 == rapiSender.sendCmdSync("$GD")) { if(rapiSender.getTokenCnt() >= 5 && (0 != String(rapiSender.getToken(1)).toInt() || @@ -208,31 +239,50 @@ void divert_update_state() { // Timer is enabled so we need to emulate a button press to work around // an issue with $FE not working - if(false == chargeStarted && 0 == rapiSender.sendCmd(F("$F1"))) { + if(false == chargeStarted && 0 == rapiSender.sendCmdSync(F("$F1"))) { DBUGLN(F("Starting charge with button press")); chargeStarted = true; } } } - if(false == chargeStarted && 0 == rapiSender.sendCmd(F("$FE"))) { + if(false == chargeStarted && 0 == rapiSender.sendCmdSync(F("$FE"))) { DBUGLN(F("Starting charge")); + chargeStarted = true; + } + + if(chargeStarted) + { + min_charge_end = divertmode_get_time() + divert_min_charge_time; + event["divert_active"] = divert_active = true; } } } - } // end ecomode + else + { + if(OPENEVSE_STATE_SLEEPING != state) + { + if(divert_active && divertmode_get_time() >= min_charge_end) + { + if(0 == rapiSender.sendCmdSync(F("$FS"))) + { + DBUGLN(F("Charge Stopped")); + event["divert_active"] = divert_active = false; - DBUGVAR(charge_rate); + if(0 == rapiSender.sendCmdSync(String(F("$SC ")) + String(max_charge_current))) { + DBUGF("Restore max I: %d", max_charge_current); + } + } + } + } + } + + event["charge_rate"] = charge_rate; + event["voltage"] = voltage; + event["avalible_current"] = avalible_current; + event["smoothed_avalible_current"] = smoothed_avalible_current; + } // end ecomode - String event = mqtt_grid_ie != "" ? F("{\"grid_ie\":") : F("{\"solar\":"); - event += mqtt_grid_ie != "" ? String(grid_ie) : String(solar); - if (divertmode == DIVERT_MODE_ECO) - { - event += F(",\"charge_rate\":"); - event += String(charge_rate); - } - event += F(",\"divert_update\":0}"); - DBUGVAR(event); event_send(event); lastUpdate = millis(); diff --git a/src/divert.h b/src/divert.h index 24542ed..2b62f7c 100644 --- a/src/divert.h +++ b/src/divert.h @@ -7,12 +7,16 @@ #include +#define DIVERT_MODE_NORMAL 1 +#define DIVERT_MODE_ECO 2 + // global variable extern byte divertmode; extern int solar; extern int grid_ie; extern int charge_rate; extern uint32_t lastUpdate; +extern bool divert_active; // Change mode void divertmode_update(byte divertmode); diff --git a/src/emoncms.cpp b/src/emoncms.cpp index 8cc3787..401353d 100644 --- a/src/emoncms.cpp +++ b/src/emoncms.cpp @@ -1,24 +1,55 @@ +#if defined(ENABLE_DEBUG) && !defined(ENABLE_DEBUG_EMONCMS) +#undef ENABLE_DEBUG +#endif + +#include +#include + #include "emonesp.h" #include "emoncms.h" -#include "config.h" +#include "app_config.h" #include "http.h" #include "input.h" - -#include - -//EMONCMS SERVER strings +#include "event.h" boolean emoncms_connected = false; +boolean emoncms_updated = false; unsigned long packets_sent = 0; unsigned long packets_success = 0; +const char *post_path = "/input/post?"; -void -emoncms_publish(String url) { +static void emoncms_result(bool success, String message) +{ + StaticJsonDocument<128> event; + + emoncms_connected = success; + event["emoncms_connected"] = (int)emoncms_connected; + event["emoncms_message"] = message.substring(0, 64); + event_send(event); +} + +void emoncms_publish(JsonDocument &data) +{ Profile_Start(emoncms_publish); - if (emoncms_apikey != 0) { + if (config_emoncms_enabled() && emoncms_apikey != 0) + { + String url = emoncms_server + post_path; + String json; + serializeJson(data, json); + url += "fulljson="; +// MongooseString encodedJson = mg_url_encode(MongooseString(json)); +// url += (const char *)encodedJson; + url += json; + url += "&node="; + url += emoncms_node; + url += "&apikey="; + url += emoncms_apikey; + + DBUGVAR(url); + DEBUG.println(emoncms_server.c_str() + String(url)); packets_sent++; // Send data to Emoncms server @@ -36,14 +67,27 @@ emoncms_publish(String url) { delay(10); result = get_http(emoncms_server.c_str(), url); } - if (result == "ok") { + + const size_t capacity = JSON_OBJECT_SIZE(2) + result.length(); + DynamicJsonDocument doc(capacity); + if(DeserializationError::Code::Ok == deserializeJson(doc, result.c_str(), result.length())) + { + DBUGLN("Got JSON"); + bool success = doc["success"]; // true + if(success) { + packets_success++; + } + emoncms_result(success, doc["message"]); + } else if (result == "ok") { packets_success++; - emoncms_connected = true; + emoncms_result(true, result); } else { - emoncms_connected = false; DEBUG.print("Emoncms error: "); DEBUG.println(result); + emoncms_result(false, result); } + } else { + emoncms_result(false, String("Disabled")); } Profile_End(emoncms_publish, 10); diff --git a/src/emoncms.h b/src/emoncms.h index 7591206..404e56f 100644 --- a/src/emoncms.h +++ b/src/emoncms.h @@ -2,12 +2,15 @@ #define _EMONESP_EMONCMS_H #include +#include // ------------------------------------------------------------------- // Commutication with EmonCMS // ------------------------------------------------------------------- extern boolean emoncms_connected; +extern boolean emoncms_updated; + extern unsigned long packets_sent; extern unsigned long packets_success; @@ -16,7 +19,7 @@ extern unsigned long packets_success; // // data: a comma seperated list of name:value pairs to send // ------------------------------------------------------------------- -void emoncms_publish(String data); +void emoncms_publish(JsonDocument &data); #endif // _EMONESP_EMONCMS_H diff --git a/src/emonesp.h b/src/emonesp.h index 445d293..780e91b 100644 --- a/src/emonesp.h +++ b/src/emonesp.h @@ -8,4 +8,48 @@ #include "debug.h" #include "profile.h" +#ifndef RAPI_PORT +#ifdef ESP32 +#define RAPI_PORT Serial1 +#elif defined(ESP8266) +#define RAPI_PORT Serial +#else +#error Platform not supported +#endif +#endif + +#ifndef DEFAULT_VOLTAGE +#define DEFAULT_VOLTAGE 240 +#endif + +#ifdef NO_SENSOR_SCALING + +#ifndef VOLTS_SCALE_FACTOR +#define VOLTS_SCALE_FACTOR 1.0 +#endif + +#ifndef AMPS_SCALE_FACTOR +#define AMPS_SCALE_FACTOR 1.0 +#endif + +#ifndef TEMP_SCALE_FACTOR +#define TEMP_SCALE_FACTOR 1.0 +#endif + +#else + +#ifndef VOLTS_SCALE_FACTOR +#define VOLTS_SCALE_FACTOR 1.0 +#endif + +#ifndef AMPS_SCALE_FACTOR +#define AMPS_SCALE_FACTOR 1000.0 +#endif + +#ifndef TEMP_SCALE_FACTOR +#define TEMP_SCALE_FACTOR 10.0 +#endif + +#endif + #endif // _EMONESP_H diff --git a/src/event.h b/src/event.h index 4e474bf..ae4f5f1 100644 --- a/src/event.h +++ b/src/event.h @@ -4,5 +4,6 @@ #include void event_send(String event); +void event_send(JsonDocument &event); #endif diff --git a/src/input.cpp b/src/input.cpp index 7a038ef..d5c7034 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -1,22 +1,25 @@ - #if defined(ENABLE_DEBUG) && !defined(ENABLE_DEBUG_RAPI) + #if defined(ENABLE_DEBUG) && !defined(ENABLE_DEBUG_INPUT) #undef ENABLE_DEBUG #endif +#include +#include +#include +#include // Connect to Wifi + +#include +#include + #include "emonesp.h" #include "input.h" -#include "config.h" +#include "app_config.h" #include "divert.h" -#include "mqtt.h" -#include "web_server.h" +#include "event.h" #include "wifi.h" #include "openevse.h" #include "RapiSender.h" -#define OPENEVSE_WIFI_MODE_AP 0 -#define OPENEVSE_WIFI_MODE_CLIENT 1 -#define OPENEVSE_WIFI_MODE_AP_DEFAULT 2 - const char *e_url = "/input/post.json?node="; String url = ""; @@ -27,11 +30,14 @@ int espfree = 0; int rapi_command = 1; -long amp = 0; // OpenEVSE Current Sensor -long volt = 0; // Not currently in used -long temp1 = 0; // Sensor DS3232 Ambient -long temp2 = 0; // Sensor MCP9808 Ambient -long temp3 = 0; // Sensor TMP007 Infared +double amp = 0; // OpenEVSE Current Sensor +double voltage = DEFAULT_VOLTAGE; // Voltage from OpenEVSE or MQTT +double temp1 = 0; // Sensor DS3232 Ambient +bool temp1_valid = false; +double temp2 = 0; // Sensor MCP9808 Ambiet +bool temp2_valid = false; +double temp3 = 0; // Sensor TMP007 Infared +bool temp3_valid = false; long pilot = 0; // OpenEVSE Pilot Setting long state = OPENEVSE_STATE_STARTING; // OpenEVSE State long elapsed = 0; // Elapsed time (only valid if charging) @@ -84,33 +90,34 @@ long watthour_total = 0; unsigned long comm_sent = 0; unsigned long comm_success = 0; -void -create_rapi_json() { +void create_rapi_json(JsonDocument &doc) +{ url = e_url; - data = ""; url += String(emoncms_node) + "&json={"; - data += "\"amp\":" + String(amp) + ","; - if (volt > 0) { - data += "volt:" + String(volt) + ","; + + doc["amp"] = amp * AMPS_SCALE_FACTOR; + doc["voltage"] = voltage * VOLTS_SCALE_FACTOR; + doc["pilot"] = pilot; + doc["wh"] = watthour_total; + if(temp1_valid) { + doc["temp1"] = temp1 * TEMP_SCALE_FACTOR; + } else { + doc["temp1"] = false; } - data += "\"wh\":" + String(watthour_total) + ","; - data += "\"temp1\":" + String(temp1) + ","; - data += "\"temp2\":" + String(temp2) + ","; - data += "\"temp3\":" + String(temp3) + ","; - data += "\"pilot\":" + String(pilot) + ","; - data += "\"state\":" + String(state) + ","; - data += "\"freeram\":" + String(ESP.getFreeHeap()) + ","; - data += "\"divertmode\":" + String(divertmode); - url += data; - if (emoncms_server == "data.openevse.com/emoncms") { - // data.openevse uses device module - url += "}&devicekey=" + emoncms_apikey; + if(temp2_valid) { + doc["temp2"] = temp2 * TEMP_SCALE_FACTOR; } else { - // emoncms.org does not use device module - url += "}&apikey=" + emoncms_apikey; + doc["temp2"] = false; } - - //DEBUG.print(emoncms_server.c_str() + String(url)); + if(temp3_valid) { + doc["temp3"] = temp3 * TEMP_SCALE_FACTOR; + } else { + doc["temp3"] = false; + } + doc["state"] = state; + doc["freeram"] = ESPAL.getFreeHeap(); + doc["divertmode"] = divertmode; + doc["srssi"] = WiFi.RSSI(); } // ------------------------------------------------------------------- @@ -125,139 +132,91 @@ void update_rapi_values() { Profile_Start(update_rapi_values); - comm_sent++; switch(rapi_command) { case 1: - if (0 == rapiSender.sendCmd("$GE")) + rapiSender.sendCmd("$GE", [](int ret) { - if(rapiSender.getTokenCnt() >= 3) + if(RAPI_RESPONSE_OK == ret) { - const char *val = rapiSender.getToken(1); - pilot = strtol(val, NULL, 10); - comm_success++; + if(rapiSender.getTokenCnt() >= 3) + { + const char *val = rapiSender.getToken(1); + pilot = strtol(val, NULL, 10); + } } - } + }); break; case 2: - if (0 == rapiSender.sendCmd("$GS")) + OpenEVSE.getStatus([](int ret, uint8_t evse_state, uint32_t session_time, uint8_t pilot_state, uint32_t vflags) { - if(rapiSender.getTokenCnt() >= 3) + if(RAPI_RESPONSE_OK == ret) { - const char *val = rapiSender.getToken(1); - DBUGVAR(val); - state = strtol(val, NULL, 10); - DBUGVAR(state); - val = rapiSender.getToken(2); - DBUGVAR(val); - elapsed = strtol(val, NULL, 10); - DBUGVAR(elapsed); - comm_success++; + DBUGF("evse_state = %02x, session_time = %d, pilot_state = %02x, vflags = %08x", evse_state, session_time, pilot_state, vflags); -#ifdef ENABLE_LEGACY_API - switch (state) { - case 1: - estate = "Not Connected"; - break; - case 2: - estate = "EV Connected"; - break; - case 3: - estate = "Charging"; - break; - case 4: - estate = "Vent Required"; - break; - case 5: - estate = "Diode Check Failed"; - break; - case 6: - estate = "GFCI Fault"; - break; - case 7: - estate = "No Earth Ground"; - break; - case 8: - estate = "Stuck Relay"; - break; - case 9: - estate = "GFCI Self Test Failed"; - break; - case 10: - estate = "Over Temperature"; - break; - case 254: - estate = "Sleeping"; - break; - case 255: - estate = "Disabled"; - break; - default: - estate = "Invalid"; - break; - } -#endif + state = evse_state; + elapsed = session_time; } - } + }); break; case 3: - if (0 == rapiSender.sendCmd("$GG")) + OpenEVSE.getChargeCurrentAndVoltage([](int ret, double a, double volts) { - if(rapiSender.getTokenCnt() >= 3) + if(RAPI_RESPONSE_OK == ret) { - const char *val; - val = rapiSender.getToken(1); - amp = strtol(val, NULL, 10); - val = rapiSender.getToken(2); - volt = strtol(val, NULL, 10); - comm_success++; + amp = a; + if(volts >= 0) { + voltage = volts; + } } - } + }); break; case 4: - if (0 == rapiSender.sendCmd("$GP")) + OpenEVSE.getTemperature([](int ret, double t1, bool t1_valid, double t2, bool t2_valid, double t3, bool t3_valid) { - if(rapiSender.getTokenCnt() >= 4) + if(RAPI_RESPONSE_OK == ret) { - const char *val; - val = rapiSender.getToken(1); - temp1 = strtol(val, NULL, 10); - val = rapiSender.getToken(2); - temp2 = strtol(val, NULL, 10); - val = rapiSender.getToken(3); - temp3 = strtol(val, NULL, 10); - comm_success++; + temp1 = t1; + temp1_valid = t1_valid; + temp2 = t2; + temp2_valid = t2_valid; + temp3 = t3; + temp3_valid = t3_valid; } - } + }); break; case 5: - if (0 == rapiSender.sendCmd("$GU")) + rapiSender.sendCmd("$GU", [](int ret) { - if(rapiSender.getTokenCnt() >= 3) + if(RAPI_RESPONSE_OK == ret) { - const char *val; - val = rapiSender.getToken(1); - wattsec = strtol(val, NULL, 10); - val = rapiSender.getToken(2); - watthour_total = strtol(val, NULL, 10); - comm_success++; + if(rapiSender.getTokenCnt() >= 3) + { + const char *val; + val = rapiSender.getToken(1); + wattsec = strtol(val, NULL, 10); + val = rapiSender.getToken(2); + watthour_total = strtol(val, NULL, 10); + } } - } + }); break; case 6: - if (0 == rapiSender.sendCmd("$GF")) { - if(rapiSender.getTokenCnt() >= 4) - { - const char *val; - val = rapiSender.getToken(1); - gfci_count = strtol(val, NULL, 16); - val = rapiSender.getToken(2); - nognd_count = strtol(val, NULL, 16); - val = rapiSender.getToken(3); - stuck_count = strtol(val, NULL, 16); - comm_success++; + rapiSender.sendCmd("$GF", [](int ret) + { + if(RAPI_RESPONSE_OK == ret) { + if(rapiSender.getTokenCnt() >= 4) + { + const char *val; + val = rapiSender.getToken(1); + gfci_count = strtol(val, NULL, 16); + val = rapiSender.getToken(2); + nognd_count = strtol(val, NULL, 16); + val = rapiSender.getToken(3); + stuck_count = strtol(val, NULL, 16); + } } - } + }); rapi_command = 0; //Last RAPI command break; } @@ -267,132 +226,88 @@ update_rapi_values() { } void -handleRapiRead() { +handleRapiRead() +{ Profile_Start(handleRapiRead); - comm_sent++; - if (0 == rapiSender.sendCmd("$GV")) - { - if(rapiSender.getTokenCnt() >= 3) + OpenEVSE.getVersion([](int ret, const char *returned_firmware, const char *returned_protocol) { + if(RAPI_RESPONSE_OK == ret) { - firmware = rapiSender.getToken(1); - protocol = rapiSender.getToken(2); - comm_success++; + firmware = returned_firmware; + protocol = returned_protocol; } - } - comm_sent++; - if (0 == rapiSender.sendCmd("$GA")) + }); + + OpenEVSE.getTime([](int ret, time_t evse_time) { - if(rapiSender.getTokenCnt() >= 3) + if(RAPI_RESPONSE_OK == ret) { - const char *val; - val = rapiSender.getToken(1); - current_scale = strtol(val, NULL, 10); - val = rapiSender.getToken(2); - current_offset = strtol(val, NULL, 10); - comm_success++; + struct timeval set_time = { evse_time, 0 }; + settimeofday(&set_time, NULL); } - } -#ifdef ENABLE_LEGACY_API - comm_sent++; - if (0 == rapiSender.sendCmd("$GH")) + }); + + rapiSender.sendCmd("$GA", [](int ret) { - if(rapiSender.getTokenCnt() >= 2) + if(RAPI_RESPONSE_OK == ret) { - const char *val; - val = rapiSender.getToken(1); - kwh_limit = strtol(val, NULL, 10); - comm_success++; + if(rapiSender.getTokenCnt() >= 3) + { + const char *val; + val = rapiSender.getToken(1); + current_scale = strtol(val, NULL, 10); + val = rapiSender.getToken(2); + current_offset = strtol(val, NULL, 10); + } } - } - comm_sent++; - if (0 == rapiSender.sendCmd("$G3")) + }); + + rapiSender.sendCmd("$GE", [](int ret) { - if(rapiSender.getTokenCnt() >= 2) + if(RAPI_RESPONSE_OK == ret) { const char *val; val = rapiSender.getToken(1); - time_limit = strtol(val, NULL, 10); - comm_success++; - } - } -#endif - comm_sent++; - if (0 == rapiSender.sendCmd("$GE")) - { - comm_success++; - const char *val; - val = rapiSender.getToken(1); - DBUGVAR(val); - pilot = strtol(val, NULL, 10); - - val = rapiSender.getToken(2); - DBUGVAR(val); - long flags = strtol(val, NULL, 16); - service = bitRead(flags, 0) + 1; - diode_ck = bitRead(flags, 1); - vent_ck = bitRead(flags, 2); - ground_ck = bitRead(flags, 3); - stuck_relay = bitRead(flags, 4); - auto_service = bitRead(flags, 5); - auto_start = bitRead(flags, 6); - serial_dbg = bitRead(flags, 7); - rgb_lcd = bitRead(flags, 8); - gfci_test = bitRead(flags, 9); - temp_ck = bitRead(flags, 10); - } - comm_sent++; -#ifdef ENABLE_LEGACY_API - if (0 == rapiSender.sendCmd("$GC")) - { - if(rapiSender.getTokenCnt() >= 3) - { - const char *val; - if (service == 1) { - val = rapiSender.getToken(1); - current_l1min = strtol(val, NULL, 10); - val = rapiSender.getToken(2); - current_l1max = strtol(val, NULL, 10); - } else { - val = rapiSender.getToken(1); - current_l2min = strtol(val, NULL, 10); - val = rapiSender.getToken(2); - current_l2max = strtol(val, NULL, 10); - } - comm_success++; + DBUGVAR(val); + pilot = strtol(val, NULL, 10); + + val = rapiSender.getToken(2); + DBUGVAR(val); + long flags = strtol(val, NULL, 16); + service = bitRead(flags, 0) + 1; + diode_ck = bitRead(flags, 1); + vent_ck = bitRead(flags, 2); + ground_ck = bitRead(flags, 3); + stuck_relay = bitRead(flags, 4); + auto_service = bitRead(flags, 5); + auto_start = bitRead(flags, 6); + serial_dbg = bitRead(flags, 7); + rgb_lcd = bitRead(flags, 8); + gfci_test = bitRead(flags, 9); + temp_ck = bitRead(flags, 10); } - } -#endif + }); + Profile_End(handleRapiRead, 10); } -void on_rapi_event() +void input_setup() { - if(!strcmp(rapiSender.getToken(0), "$ST")) { - const char *val = rapiSender.getToken(1); - DBUGVAR(val); - - // Update our local state - state = strtol(val, NULL, 16); - DBUGVAR(state); + OpenEVSE.onState([](uint8_t evse_state, uint8_t pilot_state, uint32_t current_capacity, uint32_t vflags) + { + // Update our global state + DBUGVAR(evse_state); + state = evse_state; // Send to all clients - String event = F("{\"state\":"); - event += state; - event += F("}"); - web_server_event(event); - - if (config_mqtt_enabled()) { - event = F("state:"); - event += String(state); - mqtt_publish(event); - } - } else if(!strcmp(rapiSender.getToken(0), "$WF")) { - const char *val = rapiSender.getToken(1); - DBUGVAR(val); + StaticJsonDocument<32> event; + event["state"] = state; + event_send(event); + }); - long wifiMode = strtol(val, NULL, 10); + OpenEVSE.onWiFi([](uint8_t wifiMode) + { DBUGVAR(wifiMode); switch(wifiMode) { @@ -404,5 +319,5 @@ void on_rapi_event() wifi_turn_off_ap(); break; } - } + }); } diff --git a/src/input.h b/src/input.h index 2f2fe05..c17fb56 100644 --- a/src/input.h +++ b/src/input.h @@ -2,6 +2,7 @@ #define _EMONESP_INPUT_H #include +#include #include "RapiSender.h" extern RapiSender rapiSender; @@ -9,11 +10,14 @@ extern RapiSender rapiSender; extern String url; extern String data; -extern long amp; // OpenEVSE Current Sensor -extern long volt; // Not currently in used -extern long temp1; // Sensor DS3232 Ambient -extern long temp2; // Sensor MCP9808 Ambient -extern long temp3; // Sensor TMP007 Infared +extern double amp; // OpenEVSE Current Sensor +extern double voltage; // voltage from OpenEVSE or MQTT +extern double temp1; // Sensor DS3232 Ambient +extern bool temp1_valid; +extern double temp2; // Sensor MCP9808 Ambient +extern bool temp2_valid; +extern double temp3; // Sensor TMP007 Infared +extern bool temp3_valid; extern long pilot; // OpenEVSE Pilot Setting extern long state; // OpenEVSE State extern long elapsed; // Elapsed time (only valid if charging) @@ -64,13 +68,10 @@ extern long watthour_total; extern String ohm_hour; -extern unsigned long comm_sent; -extern unsigned long comm_success; - extern void handleRapiRead(); extern void update_rapi_values(); -extern void create_rapi_json(); -extern void on_rapi_event(); +extern void create_rapi_json(JsonDocument &data); +extern void input_setup(); #endif // _EMONESP_INPUT_H diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 5ad070a..220fdc1 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -1,8 +1,11 @@ #include "emonesp.h" #include "mqtt.h" -#include "config.h" +#include "app_config.h" #include "divert.h" #include "input.h" +#include "espal.h" + +#include "openevse.h" #include #include // MQTT https://github.com/knolleary/pubsubclient PlatformIO lib: 89 @@ -11,10 +14,15 @@ WiFiClient espClient; // Create client for MQTT PubSubClient mqttclient(espClient); // Create client for MQTT -long lastMqttReconnectAttempt = 0; +static long nextMqttReconnectAttempt = 0; +static unsigned long mqttRestartTime = 0; + int clientTimeout = 0; int i = 0; -String payload_str = ""; + +#ifndef MQTT_CONNECT_TIMEOUT +#define MQTT_CONNECT_TIMEOUT (5 * 1000) +#endif // !MQTT_CONNECT_TIMEOUT // ------------------------------------------------------------------- // MQTT msg Received callback function: @@ -25,7 +33,7 @@ String payload_str = ""; void mqttmsg_callback(char *topic, byte * payload, unsigned int length) { String topic_string = String(topic); - payload_str = ""; + String payload_str = ""; // print received MQTT to debug DBUGLN("MQTT received:"); @@ -42,12 +50,18 @@ void mqttmsg_callback(char *topic, byte * payload, unsigned int length) { DBUGF("solar:%dW", solar); divert_update_state(); } - else if (topic_string == mqtt_grid_ie){ grid_ie = payload_str.toInt(); - DBUGF("grid:%dW", solar); + DBUGF("grid:%dW", grid_ie); divert_update_state(); } + else if (topic_string == mqtt_vrms){ + voltage = payload_str.toFloat(); + DBUGF("voltage:%d", voltage); + OpenEVSE.setVoltage(voltage, [](int ret) { + // Only gives better power calculations so not critical if this fails + }); + } // If MQTT message to set divert mode is received else if (topic_string == mqtt_topic + "/divertmode/set"){ byte newdivert = payload_str.toInt(); @@ -61,6 +75,7 @@ void mqttmsg_callback(char *topic, byte * payload, unsigned int length) { // Detect if MQTT message is a RAPI command e.g to set 13A /rapi/$SC 13 // Locate '$' character in the MQTT message to identify RAPI command int rapi_character_index = topic_string.indexOf('$'); + DBUGVAR(rapi_character_index); if (rapi_character_index > 1) { DBUGF("Processing as RAPI"); // Print RAPI command from mqtt-sub topic e.g $SC @@ -74,16 +89,16 @@ void mqttmsg_callback(char *topic, byte * payload, unsigned int length) { } } - comm_sent++; - if (0 == rapiSender.sendCmd(cmd.c_str())) { - comm_success++; - String rapiString = rapiSender.getResponse(); - if (rapiString.startsWith("$OK ") || rapiString.startsWith("$NK ")) { + rapiSender.sendCmd(cmd, [](int ret) + { + if (RAPI_RESPONSE_OK == ret || RAPI_RESPONSE_NK == ret) + { + String rapiString = rapiSender.getResponse(); String mqtt_data = rapiString; String mqtt_sub_topic = mqtt_topic + "/rapi/out"; mqttclient.publish(mqtt_sub_topic.c_str(), mqtt_data.c_str()); } - } + }); } } } //end call back @@ -93,7 +108,7 @@ void mqttmsg_callback(char *topic, byte * payload, unsigned int length) { // ------------------------------------------------------------------- boolean mqtt_connect() { - mqttclient.setServer(mqtt_server.c_str(), 1883); + mqttclient.setServer(mqtt_server.c_str(), mqtt_port); mqttclient.setCallback(mqttmsg_callback); //function to be called when mqtt msg is received on subscribed topic DEBUG.print("MQTT Connecting to..."); DEBUG.println(mqtt_user.c_str()); @@ -105,12 +120,19 @@ mqtt_connect() { //e.g to set current to 13A: /rapi/in/$SC 13 mqttclient.subscribe(mqtt_sub_topic.c_str()); // subscribe to solar PV / grid_ie MQTT feeds - if (mqtt_solar!=""){ - mqttclient.subscribe(mqtt_solar.c_str()); + if(config_divert_enabled()) + { + if (mqtt_solar!="") { + mqttclient.subscribe(mqtt_solar.c_str()); + } + if (mqtt_grid_ie!="") { + mqttclient.subscribe(mqtt_grid_ie.c_str()); + } } - if (mqtt_grid_ie!=""){ - mqttclient.subscribe(mqtt_grid_ie.c_str()); + if (mqtt_vrms!="") { + mqttclient.subscribe(mqtt_vrms.c_str()); } + mqtt_sub_topic = mqtt_topic + "/divertmode/set"; // MQTT Topic to change divert mode mqttclient.subscribe(mqtt_sub_topic.c_str()); @@ -128,47 +150,19 @@ mqtt_connect() { // Publish status to MQTT // ------------------------------------------------------------------- void -mqtt_publish(String data) { +mqtt_publish(JsonDocument &data) { Profile_Start(mqtt_publish); - String mqtt_data = ""; - String topic = mqtt_topic + "/"; - - int i = 0; - if(data[i] == '{') { - i++; + if(!config_mqtt_enabled() || !mqttclient.connected()) { + return; } - while (int (data[i]) != 0) { - // Construct MQTT topic e.g. / data - while (data[i] != ':') { - if(data[i] != '"') { - topic += data[i]; - } - i++; - if (int (data[i]) == 0) { - break; - } - } - i++; - // Construct data string to publish to above topic - while (data[i] != ',') { - if(data[i] != '}') { - mqtt_data += data[i]; - } - i++; - if (int (data[i]) == 0) { - break; - } - } - // send data via mqtt - //delay(100); - DEBUG.printf("%s = %s\r\n", topic.c_str(), mqtt_data.c_str()); - mqttclient.publish(topic.c_str(), mqtt_data.c_str()); - topic = mqtt_topic + "/"; - mqtt_data = ""; - i++; - if (int (data[i]) == 0) - break; + + JsonObject root = data.as(); + for (JsonPair kv : root) { + String topic = mqtt_topic + "/"; + topic += kv.key().c_str(); + String val = kv.value().as(); + mqttclient.publish(topic.c_str(), val.c_str()); } Profile_End(mqtt_publish, 5); @@ -182,28 +176,40 @@ mqtt_publish(String data) { void mqtt_loop() { Profile_Start(mqtt_loop); - if (!mqttclient.connected()) { - long now = millis(); - // try and reconnect continuously for first 5s then try again once every 10s - if ((now < 5000) || ((now - lastMqttReconnectAttempt) > 10000)) { - lastMqttReconnectAttempt = now; - if (mqtt_connect()) { // Attempt to reconnect - lastMqttReconnectAttempt = 0; + + // Do we need to restart MQTT? + if(mqttRestartTime > 0 && millis() > mqttRestartTime) + { + mqttRestartTime = 0; + if (mqttclient.connected()) { + DBUGF("Disconnecting MQTT"); + mqttclient.disconnect(); + } + nextMqttReconnectAttempt = 0; + } + + if(config_mqtt_enabled()) + { + if (!mqttclient.connected()) { + long now = millis(); + // try and reconnect every x seconds + if (now > nextMqttReconnectAttempt) { + nextMqttReconnectAttempt = now + MQTT_CONNECT_TIMEOUT; + mqtt_connect(); // Attempt to reconnect } + } else { + // if MQTT connected + mqttclient.loop(); } - } else { - // if MQTT connected - mqttclient.loop(); } + Profile_End(mqtt_loop, 5); } void mqtt_restart() { - if (mqttclient.connected()) { - mqttclient.disconnect(); - } - lastMqttReconnectAttempt = 0; + // If connected disconnect MQTT to trigger re-connect with new details + mqttRestartTime = millis(); } boolean diff --git a/src/mqtt.h b/src/mqtt.h index 57abe2c..79fef30 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -6,6 +6,7 @@ // ------------------------------------------------------------------- #include +#include extern void mqtt_msg_callback(); @@ -20,7 +21,7 @@ extern void mqtt_loop(); // // data: a comma seperated list of name:value pairs to send // ------------------------------------------------------------------- -extern void mqtt_publish(String data); +extern void mqtt_publish(JsonDocument &event); // ------------------------------------------------------------------- // Restart the MQTT connection diff --git a/src/ohm.cpp b/src/ohm.cpp index a9920a5..ab9e78b 100644 --- a/src/ohm.cpp +++ b/src/ohm.cpp @@ -1,7 +1,7 @@ #include "emonesp.h" #include "input.h" #include "wifi.h" -#include "config.h" +#include "app_config.h" #include "RapiSender.h" #include @@ -66,21 +66,29 @@ void ohm_loop() if(ohm_hour == "True") { DBUGLN(F("Ohm Hour")); - if (evse_sleep == 0) { + if (evse_sleep == 0) + { evse_sleep = 1; - if(0 == rapiSender.sendCmd(F("$FS"))) { - DBUGLN(F("Charging Started")); - } + rapiSender.sendCmd(F("$FS"), [](int ret) + { + if(RAPI_RESPONSE_OK == ret) { + DBUGLN(F("Charge Stopped")); + } + }); } } else { DBUGLN(F("It is not an Ohm Hour")); - if (evse_sleep == 1) { + if (evse_sleep == 1) + { evse_sleep = 0; - if(0 == rapiSender.sendCmd(F("$FE"))) { - DBUGLN(F("Charging Stopped")); - } + rapiSender.sendCmd(F("$FE"), [](int ret) + { + if(RAPI_RESPONSE_OK == ret) { + DBUGLN(F("Charging enabled")); + } + }); } } } diff --git a/src/openevse.h b/src/openevse.h deleted file mode 100644 index 01f4fe3..0000000 --- a/src/openevse.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef __OPENEVSE_H -#define __OPENEVSE_H - -#define OPENEVSE_STATE_INVALID -1 - -#define OPENEVSE_STATE_STARTING 0 -#define OPENEVSE_STATE_NOT_CONNECTED 1 -#define OPENEVSE_STATE_CONNECTED 2 -#define OPENEVSE_STATE_CHARGING 3 -#define OPENEVSE_STATE_VENT_REQUIRED 4 -#define OPENEVSE_STATE_DIODE_CHECK_FAILED 5 -#define OPENEVSE_STATE_GFI_FAULT 6 -#define OPENEVSE_STATE_NO_EARTH_GROUND 7 -#define OPENEVSE_STATE_STUCK_RELAY 8 -#define OPENEVSE_STATE_GFI_SELF_TEST_FAILED 9 -#define OPENEVSE_STATE_OVER_TEMPERATURE 10 -#define OPENEVSE_STATE_SLEEPING 254 -#define OPENEVSE_STATE_DISABLED 255 - -#endif diff --git a/src/ota.cpp b/src/ota.cpp index 449947f..7c01b90 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -6,7 +6,7 @@ #include #include "lcd.h" -#include "config.h" +#include "app_config.h" static int lastPercent = -1; diff --git a/src/src.ino b/src/src.ino index d1b162c..e266d78 100644 --- a/src/src.ino +++ b/src/src.ino @@ -28,7 +28,7 @@ #include // local OTA update from Arduino IDE #include "emonesp.h" -#include "config.h" +#include "app_config.h" #include "wifi.h" #include "web_server.h" #include "ohm.h" @@ -39,52 +39,53 @@ #include "divert.h" #include "ota.h" #include "lcd.h" +#include "espal.h" +#include "event.h" #include "RapiSender.h" -RapiSender rapiSender(&Serial); +RapiSender rapiSender(&RAPI_PORT); unsigned long Timer1; // Timer for events once every 30 seconds unsigned long Timer3; // Timer for events once every 2 seconds boolean rapi_read = 0; //flag to indicate first read of RAPI status +static void hardware_setup(); + // ------------------------------------------------------------------- // SETUP // ------------------------------------------------------------------- -void setup() { - delay(2000); - Serial.begin(115200); - pinMode(0, INPUT); - - DEBUG_BEGIN(115200); +void setup() +{ + hardware_setup(); + ESPAL.begin(); DEBUG.println(); - DEBUG.print("OpenEVSE WiFI "); - DEBUG.println(ESP.getChipId()); - DEBUG.println("Firmware: " + currentfirmware); - - DEBUG.printf("Free: %d\n", ESP.getFreeHeap()); + DEBUG.printf("OpenEVSE WiFI %s\n", ESPAL.getShortId().c_str()); + DEBUG.printf("Firmware: %s\n", currentfirmware.c_str()); + DEBUG.printf("IDF version: %s\n", ESP.getSdkVersion()); + DEBUG.printf("Free: %d\n", ESPAL.getFreeHeap()); // Read saved settings from the config config_load_settings(); - DBUGF("After config_load_settings: %d", ESP.getFreeHeap()); + DBUGF("After config_load_settings: %d", ESPAL.getFreeHeap()); // Initialise the WiFi wifi_setup(); - DBUGF("After wifi_setup: %d", ESP.getFreeHeap()); + DBUGF("After wifi_setup: %d", ESPAL.getFreeHeap()); // Bring up the web server web_server_setup(); - DBUGF("After web_server_setup: %d", ESP.getFreeHeap()); + DBUGF("After web_server_setup: %d", ESPAL.getFreeHeap()); #ifdef ENABLE_OTA ota_setup(); - DBUGF("After ota_setup: %d", ESP.getFreeHeap()); + DBUGF("After ota_setup: %d", ESPAL.getFreeHeap()); #endif - rapiSender.setOnEvent(on_rapi_event); - rapiSender.enableSequenceId(0); + input_setup(); + } // end setup // ------------------------------------------------------------------- @@ -103,28 +104,31 @@ loop() { rapiSender.loop(); divert_current_loop(); - if(OPENEVSE_STATE_STARTING != state && - OPENEVSE_STATE_INVALID != state) + if(OpenEVSE.isConnected()) { - // Read initial state from OpenEVSE - if (rapi_read == 0) + if(OPENEVSE_STATE_STARTING != state && + OPENEVSE_STATE_INVALID != state) { - lcd_display(F("OpenEVSE WiFI"), 0, 0, 0, LCD_CLEAR_LINE); - lcd_display(currentfirmware, 0, 1, 5 * 1000, LCD_CLEAR_LINE); - lcd_loop(); + // Read initial state from OpenEVSE + if (rapi_read == 0) + { + lcd_display(F("OpenEVSE WiFI"), 0, 0, 0, LCD_CLEAR_LINE); + lcd_display(currentfirmware, 0, 1, 5 * 1000, LCD_CLEAR_LINE); + lcd_loop(); - DBUGLN("first read RAPI values"); - handleRapiRead(); //Read all RAPI values - rapi_read=1; - } + DBUGLN("first read RAPI values"); + handleRapiRead(); //Read all RAPI values + rapi_read=1; + } - // ------------------------------------------------------------------- - // Do these things once every 2s - // ------------------------------------------------------------------- - if ((millis() - Timer3) >= 2000) { - DEBUG.printf("Free: %d\n", ESP.getFreeHeap()); - update_rapi_values(); - Timer3 = millis(); + // ------------------------------------------------------------------- + // Do these things once every 2s + // ------------------------------------------------------------------- + if ((millis() - Timer3) >= 2000) { + DEBUG.printf("Free: %d\n", ESPAL.getFreeHeap()); + update_rapi_values(); + Timer3 = millis(); + } } } else @@ -133,26 +137,24 @@ loop() { if ((millis() - Timer3) >= 1000) { // Check state the OpenEVSE is in. - if (0 == rapiSender.sendCmd("$GS")) + OpenEVSE.begin(rapiSender, [](bool connected) { - if(rapiSender.getTokenCnt() >= 3) + if(connected) { - const char *val = rapiSender.getToken(1); - DBUGVAR(val); - state = strtol(val, NULL, 10); - DBUGVAR(state); + OpenEVSE.getStatus([](int ret, uint8_t evse_state, uint32_t session_time, uint8_t pilot_state, uint32_t vflags) { + state = evse_state; + }); + } else { + DBUGLN("OpenEVSE not responding or not connected"); } - } else { - DBUGLN("OpenEVSE not responding or not connected"); - } + }); + Timer3 = millis(); } } if(wifi_client_connected()) { - if (config_mqtt_enabled()) { - mqtt_loop(); - } + mqtt_loop(); // ------------------------------------------------------------------- // Do these things once every 30 seconds @@ -160,29 +162,58 @@ loop() { if ((millis() - Timer1) >= 30000) { DBUGLN("Time1"); - create_rapi_json(); // create JSON Strings for EmonCMS and MQTT - if (config_emoncms_enabled()) { - emoncms_publish(url); - } - if (config_mqtt_enabled()) { - mqtt_publish(data); - } - if(config_ohm_enabled()) { - ohm_loop(); + if(!Update.isRunning()) + { + DynamicJsonDocument data(4096); + create_rapi_json(data); // create JSON Strings for EmonCMS and MQTT + emoncms_publish(data); + event_send(data); + + if(config_ohm_enabled()) { + ohm_loop(); + } } + Timer1 = millis(); } + + if(emoncms_updated) + { + // Send the current state to check the config + DynamicJsonDocument data(4096); + create_rapi_json(data); + emoncms_publish(data); + emoncms_updated = false; + } } // end WiFi connected Profile_End(loop, 10); } // end loop -void event_send(String event) +void event_send(String &json) +{ + StaticJsonDocument<512> event; + deserializeJson(event, json); + event_send(event); +} + +void event_send(JsonDocument &event) { + #ifdef ENABLE_DEBUG + serializeJson(event, DEBUG_PORT); + DBUGLN(""); + #endif web_server_event(event); + mqtt_publish(event); +} + +void hardware_setup() +{ + Serial.begin(115200); + DEBUG_BEGIN(115200); + + pinMode(0, INPUT); + - if (config_mqtt_enabled()) { - mqtt_publish(event); - } } diff --git a/src/web_server.cpp b/src/web_server.cpp index 14b15b9..d12e162 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -9,13 +9,14 @@ #include "emonesp.h" #include "web_server.h" #include "web_server_static.h" -#include "config.h" +#include "app_config.h" #include "wifi.h" #include "mqtt.h" #include "input.h" #include "emoncms.h" #include "divert.h" #include "lcd.h" +#include "espal.h" AsyncWebServer server(80); // Create class for Web server AsyncWebSocket ws("/ws"); @@ -38,9 +39,7 @@ const char _CONTENT_TYPE_JSON[] PROGMEM = "application/json"; const char _CONTENT_TYPE_JS[] PROGMEM = "application/javascript"; const char _CONTENT_TYPE_JPEG[] PROGMEM = "image/jpeg"; const char _CONTENT_TYPE_PNG[] PROGMEM = "image/png"; - -static const char _DUMMY_PASSWORD[] PROGMEM = "_DUMMY_PASSWORD"; -#define DUMMY_PASSWORD FPSTR(_DUMMY_PASSWORD) +const char _CONTENT_TYPE_SVG[] PROGMEM = "image/svg+xml"; // Get running firmware version from build tag environment variable #define TEXTIFY(A) #A @@ -122,34 +121,11 @@ bool isPositive(const String &str) { return str == "1" || str == "true"; } -// ------------------------------------------------------------------- -// Load Home page -// url: / -// ------------------------------------------------------------------- -/* -void -handleHome(AsyncWebServerRequest *request) { - if (www_username != "" - && wifi_mode == WIFI_MODE_STA - && !request->authenticate(www_username.c_str(), - www_password.c_str())) { - return request->requestAuthentication(esp_hostname); - } - - String page = String((wifi_mode == WIFI_MODE_AP_ONLY) ? WIFI_PAGE : HOME_PAGE); - - if (SPIFFS.exists(page)) { - request->send(SPIFFS, page); - } else { - request->send(200, CONTENT_TYPE_HTML, - F("" - "

OpenEVSE WiFi

" - "

/home.html not found, have you flashed the SPIFFS?

" - "" - "")); - } +bool isPositive(AsyncWebServerRequest *request, const char *param) { + char paramValue[8]; + int paramFound = request->hasArg(param); + return paramFound >= 0 && (0 == paramFound || isPositive(String(paramValue))); } -*/ // ------------------------------------------------------------------- // Wifi scan /scan not currently used @@ -255,9 +231,6 @@ handleSaveNetwork(AsyncWebServerRequest *request) { String qsid = request->arg("ssid"); String qpass = request->arg("pass"); - if(qpass.equals(DUMMY_PASSWORD)) { - qpass = epass; - } if (qsid != 0) { config_save_wifi(qsid, qpass); @@ -284,15 +257,10 @@ handleSaveEmoncms(AsyncWebServerRequest *request) { return; } - String apikey = request->arg("apikey"); - if(apikey.equals(DUMMY_PASSWORD)) { - apikey = emoncms_apikey; - } - config_save_emoncms(isPositive(request->arg("enable")), request->arg("server"), request->arg("node"), - apikey, + request->arg("apikey"), request->arg("fingerprint")); char tmpStr[200]; @@ -320,12 +288,16 @@ handleSaveMqtt(AsyncWebServerRequest *request) { } String pass = request->arg("pass"); - if(pass.equals(DUMMY_PASSWORD)) { - pass = mqtt_pass; + + int port = 1883; + AsyncWebParameter *portParm = request->getParam("port"); + if(nullptr != portParm) { + port = portParm->value().toInt(); } config_save_mqtt(isPositive(request->arg("enable")), request->arg("server"), + port, request->arg("topic"), request->arg("user"), pass, @@ -333,7 +305,7 @@ handleSaveMqtt(AsyncWebServerRequest *request) { request->arg("grid_ie")); char tmpStr[200]; - snprintf(tmpStr, sizeof(tmpStr), "Saved: %s %s %s %s", mqtt_server.c_str(), + snprintf(tmpStr, sizeof(tmpStr), "Saved: %s %s %s %s %s %s", mqtt_server.c_str(), mqtt_topic.c_str(), mqtt_user.c_str(), mqtt_pass.c_str(), mqtt_solar.c_str(), mqtt_grid_ie.c_str()); DBUGLN(tmpStr); @@ -343,7 +315,7 @@ handleSaveMqtt(AsyncWebServerRequest *request) { request->send(response); // If connected disconnect MQTT to trigger re-connect with new details - mqttRestartTime = millis(); + mqtt_restart(); } // ------------------------------------------------------------------- @@ -379,9 +351,6 @@ handleSaveAdmin(AsyncWebServerRequest *request) { String quser = request->arg("user"); String qpass = request->arg("pass"); - if(qpass.equals(DUMMY_PASSWORD)) { - qpass = www_password; - } config_save_admin(quser, qpass); @@ -442,78 +411,80 @@ handleStatus(AsyncWebServerRequest *request) { return; } + const size_t capacity = JSON_OBJECT_SIZE(40) + 1024; + DynamicJsonDocument doc(capacity); + String s = "{"; if (wifi_mode_is_sta_only()) { - s += "\"mode\":\"STA\","; + doc["mode"] = "STA"; } else if (wifi_mode_is_ap_only()) { - s += "\"mode\":\"AP\","; + doc["mode"] = "AP"; } else if (wifi_mode_is_ap() && wifi_mode_is_sta()) { - s += "\"mode\":\"STA+AP\","; + doc["mode"] = "STA+AP"; } - s += "\"wifi_client_connected\":" + String(wifi_client_connected()) + ","; - s += "\"srssi\":" + String(WiFi.RSSI()) + ","; - s += "\"ipaddress\":\"" + ipaddress + "\","; + doc["wifi_client_connected"] = (int)wifi_client_connected(); + doc["net_connected"] = (int)wifi_client_connected(); + doc["srssi"] = WiFi.RSSI(); + doc["ipaddress"] = ipaddress; - s += "\"emoncms_connected\":" + String(emoncms_connected) + ","; - s += "\"packets_sent\":" + String(packets_sent) + ","; - s += "\"packets_success\":" + String(packets_success) + ","; + doc["emoncms_connected"] = (int)emoncms_connected; + doc["packets_sent"] = packets_sent; + doc["packets_success"] = packets_success; - s += "\"mqtt_connected\":" + String(mqtt_connected()) + ","; + doc["mqtt_connected"] = (int)mqtt_connected(); - s += "\"ohm_hour\":\"" + ohm_hour + "\","; + doc["ohm_hour"] = ohm_hour; - s += "\"free_heap\":" + String(ESP.getFreeHeap()) + ","; + doc["free_heap"] = ESPAL.getFreeHeap(); - s += "\"comm_sent\":" + String(comm_sent) + ","; - s += "\"comm_success\":" + String(comm_success) + ","; + doc["comm_sent"] = rapiSender.getSent(); + doc["comm_success"] = rapiSender.getSuccess(); + doc["rapi_connected"] = (int)rapiSender.isConnected(); - s += "\"amp\":" + String(amp) + ","; - s += "\"pilot\":" + String(pilot) + ","; - s += "\"temp1\":" + String(temp1) + ","; - s += "\"temp2\":" + String(temp2) + ","; - s += "\"temp3\":" + String(temp3) + ","; - s += "\"state\":" + String(state) + ","; - s += "\"elapsed\":" + String(elapsed) + ","; - s += "\"wattsec\":" + String(wattsec) + ","; - s += "\"watthour\":" + String(watthour_total) + ","; + doc["amp"] = amp * AMPS_SCALE_FACTOR; + doc["voltage"] = voltage * VOLTS_SCALE_FACTOR; + doc["pilot"] = pilot; + if(temp1_valid) { + doc["temp1"] = temp1 * TEMP_SCALE_FACTOR; + } else { + doc["temp1"] = false; + } + if(temp2_valid) { + doc["temp2"] = temp2 * TEMP_SCALE_FACTOR; + } else { + doc["temp2"] = false; + } + if(temp3_valid) { + doc["temp3"] = temp3 * TEMP_SCALE_FACTOR; + } else { + doc["temp3"] = false; + } + doc["state"] = state; + doc["elapsed"] = elapsed; + doc["wattsec"] = wattsec; + doc["watthour"] = watthour_total; - s += "\"gfcicount\":" + String(gfci_count) + ","; - s += "\"nogndcount\":" + String(nognd_count) + ","; - s += "\"stuckcount\":" + String(stuck_count) + ","; + doc["gfcicount"] = gfci_count; + doc["nogndcount"] = nognd_count; + doc["stuckcount"] = stuck_count; + + doc["divertmode"] = divertmode; + doc["solar"] = solar; + doc["grid_ie"] = grid_ie; + doc["charge_rate"] = charge_rate; + doc["divert_update"] = (millis() - lastUpdate) / 1000; + + doc["ota_update"] = (int)Update.isRunning(); - s += "\"divertmode\":" + String(divertmode) + ","; - s += "\"solar\":" + String(solar) + ","; - s += "\"grid_ie\":" + String(grid_ie) + ","; - s += "\"charge_rate\":" + String(charge_rate) + ","; - s += "\"divert_update\":" + String((millis() - lastUpdate) / 1000); -#ifdef ENABLE_LEGACY_API - s += ",\"networks\":[" + st + "]"; - s += ",\"rssi\":[" + rssi + "]"; - s += ",\"version\":\"" + currentfirmware + "\""; - s += ",\"ssid\":\"" + esid + "\""; - //s += ",\"pass\":\""+epass+"\""; security risk: DONT RETURN PASSWORDS - s += ",\"emoncms_server\":\"" + emoncms_server + "\""; - s += ",\"emoncms_node\":\"" + emoncms_node + "\""; - //s += ",\"emoncms_apikey\":\""+emoncms_apikey+"\""; security risk: DONT RETURN APIKEY - s += ",\"emoncms_fingerprint\":\"" + emoncms_fingerprint + "\""; - s += ",\"mqtt_server\":\"" + mqtt_server + "\""; - s += ",\"mqtt_topic\":\"" + mqtt_topic + "\""; - s += ",\"mqtt_user\":\"" + mqtt_user + "\""; - //s += ",\"mqtt_pass\":\""+mqtt_pass+"\""; security risk: DONT RETURN PASSWORDS - s += ",\"www_username\":\"" + www_username + "\""; - //s += ",\"www_password\":\""+www_password+"\""; security risk: DONT RETURN PASSWORDS - s += ",\"ohmkey\":\"" + ohm + "\""; -#endif - s += "}"; DBUGVAR(lastUpdate); DBUGVAR(millis()); DBUGVAR((millis() - lastUpdate) / 1000); response->setCode(200); - response->print(s); + serializeJson(doc, *response); request->send(response); } @@ -528,71 +499,28 @@ handleConfig(AsyncWebServerRequest *request) { return; } - String dummyPassword = String(DUMMY_PASSWORD); + const size_t capacity = JSON_OBJECT_SIZE(40) + 1024; + DynamicJsonDocument doc(capacity); - String s = "{"; - s += "\"firmware\":\"" + firmware + "\","; - s += "\"protocol\":\"" + protocol + "\","; - s += "\"espflash\":" + String(ESP.getFlashChipSize()) + ","; - s += "\"version\":\"" + currentfirmware + "\","; - s += "\"diodet\":" + String(diode_ck) + ","; - s += "\"gfcit\":" + String(gfci_test) + ","; - s += "\"groundt\":" + String(ground_ck) + ","; - s += "\"relayt\":" + String(stuck_relay) + ","; - s += "\"ventt\":" + String(vent_ck) + ","; - s += "\"tempt\":" + String(temp_ck) + ","; - s += "\"service\":" + String(service) + ","; -#ifdef ENABLE_LEGACY_API - s += "\"l1min\":\"" + current_l1min + "\","; - s += "\"l1max\":\"" + current_l1max + "\","; - s += "\"l2min\":\"" + current_l2min + "\","; - s += "\"l2max\":\"" + current_l2max + "\","; - s += "\"kwhlimit\":\"" + kwh_limit + "\","; - s += "\"timelimit\":\"" + time_limit + "\","; - s += "\"gfcicount\":" + String(gfci_count) + ","; - s += "\"nogndcount\":" + String(nognd_count) + ","; - s += "\"stuckcount\":" + String(stuck_count) + ","; -#endif - s += "\"scale\":" + String(current_scale) + ","; - s += "\"offset\":" + String(current_offset) + ","; - s += "\"ssid\":\"" + esid + "\","; - s += "\"pass\":\""; - if(epass != 0) { - s += dummyPassword; - } - s += "\","; - s += "\"emoncms_enabled\":" + String(config_emoncms_enabled() ? "true" : "false") + ","; - s += "\"emoncms_server\":\"" + emoncms_server + "\","; - s += "\"emoncms_node\":\"" + emoncms_node + "\","; - s += "\"emoncms_apikey\":\""; - if(emoncms_apikey != 0) { - s += dummyPassword; - } - s += "\","; - s += "\"emoncms_fingerprint\":\"" + emoncms_fingerprint + "\","; - s += "\"mqtt_enabled\":" + String(config_mqtt_enabled() ? "true" : "false") + ","; - s += "\"mqtt_server\":\"" + mqtt_server + "\","; - s += "\"mqtt_topic\":\"" + mqtt_topic + "\","; - s += "\"mqtt_user\":\"" + mqtt_user + "\","; - s += "\"mqtt_pass\":\""; - if(mqtt_pass != 0) { - s += dummyPassword; - } - s += "\","; - s += "\"mqtt_solar\":\""+mqtt_solar+"\","; - s += "\"mqtt_grid_ie\":\""+mqtt_grid_ie+"\","; - s += "\"www_username\":\"" + www_username + "\","; - s += "\"www_password\":\""; - if(www_password != 0) { - s += dummyPassword; - } - s += "\","; - s += "\"hostname\":\"" + esp_hostname + "\","; - s += "\"ohm_enabled\":" + String(config_ohm_enabled() ? "true" : "false"); - s += "}"; + // EVSE Config + doc["firmware"] = firmware; + doc["protocol"] = protocol; + doc["espflash"] = ESPAL.getFlashChipSize(); + doc["version"] = currentfirmware; + doc["diodet"] = diode_ck; + doc["gfcit"] = gfci_test; + doc["groundt"] = ground_ck; + doc["relayt"] = stuck_relay; + doc["ventt"] = vent_ck; + doc["tempt"] = temp_ck; + doc["service"] = service; + doc["scale"] = current_scale; + doc["offset"] = current_offset; + + config_serialize(doc, true, false, true); response->setCode(200); - response->print(s); + serializeJson(doc, *response); request->send(response); } @@ -646,7 +574,7 @@ handleRst(AsyncWebServerRequest *request) { } config_reset(); - ESP.eraseConfig(); + ESPAL.eraseConfig(); response->setCode(200); response->print("1"); @@ -793,7 +721,9 @@ String delayTimer = "0 0 0 0"; void handleRapi(AsyncWebServerRequest *request) { - bool json = request->hasArg("json"); + bool json = isPositive(request, "json"); + + int code = 200; AsyncResponseStream *response; if(false == requestPreProcess(request, response, json ? CONTENT_TYPE_JSON : CONTENT_TYPE_HTML)) { @@ -817,23 +747,21 @@ handleRapi(AsyncWebServerRequest *request) { String rapi = request->arg("rapi"); // BUG: Really we should do this in the main loop not here... - Serial.flush(); - comm_sent++; - int ret = rapiSender.sendCmd(rapi.c_str()); + RAPI_PORT.flush(); + DBUGVAR(rapi); + int ret = rapiSender.sendCmdSync(rapi); + DBUGVAR(ret); - // IMPROVE: handle other errors, eg timeout - if(0 == ret || 1 == ret) + if(RAPI_RESPONSE_OK == ret || + RAPI_RESPONSE_NK == ret) { String rapiString = rapiSender.getResponse(); - if(0 == ret) { - comm_success++; - } // Fake $GD if not supported by firmware - if(0 == ret && rapi.startsWith(F("$ST"))) { + if(RAPI_RESPONSE_OK == ret && rapi.startsWith(F("$ST"))) { delayTimer = rapi.substring(4); } - if(1 == ret) + if(RAPI_RESPONSE_NK == ret) { if(rapi.equals(F("$GD"))) { ret = 0; @@ -849,14 +777,10 @@ handleRapi(AsyncWebServerRequest *request) { DBUGF("Attempting %s", fallback.c_str()); - comm_sent++; - int ret = rapiSender.sendCmd(fallback.c_str()); - if(0 == ret) + int ret = rapiSender.sendCmdSync(fallback.c_str()); + if(RAPI_RESPONSE_OK == ret) { String rapiString = rapiSender.getResponse(); - if(0 == ret) { - comm_success++; - } } } } @@ -869,13 +793,38 @@ handleRapi(AsyncWebServerRequest *request) { s += rapiString; } } - } + else + { + String errorString = + RAPI_RESPONSE_QUEUE_FULL == ret ? F("RAPI_RESPONSE_QUEUE_FULL") : + RAPI_RESPONSE_BUFFER_OVERFLOW == ret ? F("RAPI_RESPONSE_BUFFER_OVERFLOW") : + RAPI_RESPONSE_TIMEOUT == ret ? F("RAPI_RESPONSE_TIMEOUT") : + RAPI_RESPONSE_OK == ret ? F("RAPI_RESPONSE_OK") : + RAPI_RESPONSE_NK == ret ? F("RAPI_RESPONSE_NK") : + RAPI_RESPONSE_INVALID_RESPONSE == ret ? F("RAPI_RESPONSE_INVALID_RESPONSE") : + RAPI_RESPONSE_CMD_TOO_LONG == ret ? F("RAPI_RESPONSE_CMD_TOO_LONG") : + RAPI_RESPONSE_BAD_CHECKSUM == ret ? F("RAPI_RESPONSE_BAD_CHECKSUM") : + RAPI_RESPONSE_BAD_SEQUENCE_ID == ret ? F("RAPI_RESPONSE_BAD_SEQUENCE_ID") : + RAPI_RESPONSE_ASYNC_EVENT == ret ? F("RAPI_RESPONSE_ASYNC_EVENT") : + F("UNKNOWN"); + + if (json) { + s = "{\"cmd\":\""+rapi+"\",\"error\":\""+errorString+"\"}"; + } else { + s += rapi; + s += F("

Error:"); + s += errorString; + } + + code = 500; + } +} if (false == json) { s += F(""); s += F("

\r\n\r\n"); } - response->setCode(200); + response->setCode(code); response->print(s); request->send(response); } @@ -1016,7 +965,9 @@ web_server_loop() { Profile_End(web_server_loop, 5); } -void web_server_event(String &event) +void web_server_event(JsonDocument &event) { - ws.textAll(event); + String json; + serializeJson(event, json); + ws.textAll(json); } diff --git a/src/web_server.h b/src/web_server.h index 580e99f..bda533f 100644 --- a/src/web_server.h +++ b/src/web_server.h @@ -4,6 +4,7 @@ #include #include #include +#include // Content Types extern const char _CONTENT_TYPE_HTML[]; @@ -27,13 +28,16 @@ extern const char _CONTENT_TYPE_JPEG[]; extern const char _CONTENT_TYPE_PNG[]; #define CONTENT_TYPE_PNG FPSTR(_CONTENT_TYPE_PNG) +extern const char _CONTENT_TYPE_SVG[]; +#define CONTENT_TYPE_SVG FPSTR(_CONTENT_TYPE_SVG) + extern AsyncWebServer server; extern String currentfirmware; extern void web_server_setup(); extern void web_server_loop(); -extern void web_server_event(String &event); +extern void web_server_event(JsonDocument &event); void dumpRequest(AsyncWebServerRequest *request); diff --git a/src/web_server_static.cpp b/src/web_server_static.cpp index ef265e3..de5ee2c 100644 --- a/src/web_server_static.cpp +++ b/src/web_server_static.cpp @@ -7,7 +7,7 @@ #include "emonesp.h" #include "web_server.h" #include "web_server_static.h" -#include "config.h" +#include "app_config.h" #include "wifi.h" // Static files diff --git a/src/web_static/web_server.assets.js.h b/src/web_static/web_server.assets.js.h index 5823eef..11abecf 100644 --- a/src/web_static/web_server.assets.js.h +++ b/src/web_static/web_server.assets.js.h @@ -1,2 +1,2 @@ static const char CONTENT_ASSETS_JS[] PROGMEM = - "!function(t){var r={};function o(e){if(r[e])return r[e].exports;var n=r[e]={i:e,l:!1,exports:{}};return t[e].call(n.exports,n,n.exports,o),n.l=!0,n.exports}o.m=t,o.c=r,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},o.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},o.t=function(n,e){if(1&e&&(n=o(n)),8&e)return n;if(4&e&&\"object\"==typeof n&&n&&n.__esModule)return n;var t=Object.create(null);if(o.r(t),Object.defineProperty(t,\"default\",{enumerable:!0,value:n}),2&e&&\"string\"!=typeof n)for(var r in n)o.d(t,r,function(e){return n[e]}.bind(null,r));return t},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,\"a\",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p=\"\",o(o.s=0)}([function(e,n,t){\"use strict\";t.r(n);t(1),t(2),t(3),t(4),t(5)},function(e,n,t){},function(e,n,t){e.exports=t.p+\"emoncms.jpg\"},function(e,n,t){e.exports=t.p+\"favicon-16x16.png\"},function(e,n,t){e.exports=t.p+\"favicon-32x32.png\"},function(e,n,t){e.exports=t.p+\"ohm.jpg\"}]);\n"; + "!function(r){var n={};function o(e){if(n[e])return n[e].exports;var t=n[e]={i:e,l:!1,exports:{}};return r[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=r,o.c=n,o.d=function(e,t,r){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},o.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&\"object\"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(o.r(r),Object.defineProperty(r,\"default\",{enumerable:!0,value:t}),2&e&&\"string\"!=typeof t)for(var n in t)o.d(r,n,function(e){return t[e]}.bind(null,n));return r},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,\"a\",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p=\"\",o(o.s=0)}([function(e,t,r){\"use strict\";r.r(t);r(1)},function(e,t,r){}]);\n"; diff --git a/src/web_static/web_server.home.html.h b/src/web_static/web_server.home.html.h index 333705e..e18fabb 100644 --- a/src/web_static/web_server.home.html.h +++ b/src/web_static/web_server.home.html.h @@ -1,34 +1,48 @@ static const char CONTENT_HOME_HTML[] PROGMEM = - " OpenEVSE

OpenEVSE

WiFi

Loading, please wait... (/)

WiFi Setup

Mode:

Network RSSI dBm

IP Address:

Successful packets:
of

OpenEVSE

RAPI packets:
of

Connect to network:

Select Network RSSI dBm
Scanning...

Passkey:

Connecting to WIFI Network...

Administration

Username:
15 characters max

Password:
15 characters max
Web interface HTTP authentication.

WiFi Firmware

ESP8266

Version:

Developer Mode

Enabled:

  Energy Monitoring

Emoncms Server*:

e.g: data.openevse.com/emoncms, emoncms.org, emonpi/emoncms

Emoncms Node*:

Emoncms write-apikey*:

Emoncms SSL SHA-1 Fingerprint (optional):


HTTPS will be enabled if present e.g: 7D:82:15:BE:D7:BC:72:58:87:7D:8E:40:D4:80:BA:1A:9F:8B:8D:DA

  Connected:    Successful posts: 

MQTT

Status published to:
{base-topic}/{status} value
e.g. /amp 16

RAPI control subscribes to:
{base-topic}/rapi/in/{command} value
e.g. /rapi/in/$SC 16
e.g. /rapi/in/$GC

RAPI response published to:
{base-topic}/rapi/out response
e.g. /rapi/out $OK 6 32

Host*:

e.g 'emonpi', 'test.mosquitto.org', '192.168.1.4'

Username: blank - no authentication

Password: blank - no authentication

Base-topic*:

e.g 'openevse'

Solar PV divert

Dynamically adjust charge rate based on solar PV generation or excess power (grid export).

1. Normal (default):

  • Charge at maximum current set by EVSE.
2. Eco:
  • If only solar PV feed available: charge rate is modulated based on solar PV generation.
  • If grid +I/-E (positive Import / negative Export) feed is available: charge rate will be modulated by available excess power.
  • If EVSE is sleeping: charging will begin when solar PV / excess power > min charge rate.
  • Charging will not pause; this avoids excess wear on the EVSE relay and the EV.
Note: It's assumed that EVSE power is included in the grid feed.

SolarPV-gen topic:
Solar PV MQTT topic to modulate charge rate based on solar

Grid (+I/-E) topic:

Grid (+I/-E) MQTT topic to modulate charge rate based on excess power

  Connected: 

  OhmConnect

Click Here to Join

OhmConnect monitors real-time conditions on the electricity grid. When dirty and unsustainable power plants turn on, our users receive a notification to save energy.

Ohm Hour:

Ohm key:

USA - California only

Ohm Key can be obtained by logging in to OhmConnect, enter Settings and locate the link in \"Open Source Projects\"
Example: https://login.ohmconnect.com/verify-ohm-hour/OpnEoVse
Key: OpnEoVse

EVSE Error

OpenEVSE

OpenEVSE

WiFi

Loading, please wait... (/)

WiFi Setup

Mode:

IP Address:

Successful packets:
of

OpenEVSE

RAPI packets:
of

Network RSSI dBm

IP Address:

Successful packets:
of

OpenEVSE

RAPI packets:
of

Connect to network:

Scanning...

SSID:

Passkey:

Connecting to WIFI Network...

Administration

Username:
15 characters max

Password:


15 characters max
Web interface HTTP authentication.

WiFi Firmware

ESP8266

Version:

Error:

Updating...
Firmware update completed ok

Advanced Settings

Hostname:
31 characters max

NTP Server:

Developer Mode

Enabled:

  Energy Monitoring

Emoncms Server*:
0 }\">

Emoncms Node*:

Emoncms write-apikey*:

Emoncms SSL SHA-1 Fingerprint (optional):


HTTPS will be enabled if present e.g: 7D:82:15:BE:D7:BC:72:58:87:7D:8E:40:D4:80:BA:1A:9F:8B:8D:DA

  Connected:    Successful posts: 

MQTT

Status published to:
{base-topic}/{status} value
e.g. /amp 16

RAPI control subscribes to:
{base-topic}/rapi/in/{command} value
e.g. /rapi/in/$SC 16
e.g. /rapi/in/$GC

RAPI response published to:
{base-topic}/rapi/out response
e.g. /rapi/out $OK 6 32

Host*:
0 }\">
e.g 'emonpi', 'test.mosquitto.org', '192.168.1.4'

Port*:

Reject self-signed certificates:

Warning!!

Certificate validation is disabled, although the connection to MQTT server will be encrypted the connection is still vunrable to man-in-the-middle attacks.

Username: blank - no authentication

Password: blank - no authentication

Base-topic*:

e.g 'openevse'

Voltage topic:
Voltage MQTT topic to improve power calculations

  Connected: 

  OhmConnect

Click Here to Join

OhmConnect monitors real-time conditions on the electricity grid. When dirty and unsustainable power plants turn on, our users receive a notification to save energy.

Ohm Hour:

Ohm key:

USA - California only

Ohm Key can be obtained by logging in to OhmConnect, enter Settings and locate the link in \"Open Source Projects\"
Example: https://login.ohmconnect.com/verify-ohm-hour/OpnEoVse
Key: OpnEoVse

Solar PV divert

MQTT not enabled.

Solar PV Divert requires an SolarPV-gen or Grid (+I/-E) feed to be delivered via MQTT.
Dynamically adjust charge rate based on solar PV generation or excess power (grid export).

  • If only solar PV feed available: charge rate is modulated based on solar PV generation.
  • If grid +I/-E (positive Import / negative Export) feed is available: charge rate will be modulated by available excess power.
  • If EVSE is sleeping: charging will begin when solar PV / excess power > min charge rate.
  • Charging will pause if the excess power drops below the nim charge rate for a period of time.
Note: It's assumed that EVSE power is included in the grid feed


Solar: Grid Import/Export: W - | Charge rate: A

Feed*:

Solar PV MQTT topic to modulate charge rate based on solar Grid (+I/-E) MQTT topic to modulate charge rate based on excess power

Divert smoothing attack:

The amount of the new feed value to add to the divert avalible power rolling average

Divert smoothing decay:

The amount of the new feed value to remove to the divert avalible power rolling average

Minimum charge time:

The minimum amount of time (seconds) to charge the car once enabled via the Solar PV divert. This can help minimise wear and tare on the EVSE.

EVSE Error

EVSE Error

OpenEVSE not responding or not connected

Session Status

Current Energy Temp Elapsed

Charge Options


Time limit:

Energy limit:



Timer:
Start:    Stop:

Charge Options

Normal (fast) Eco (PV divert)


Time limit:

Energy limit:



Timer:
Start:    Stop:

Energy

Energy
This Session:
Total:

Charge Mode

MQTT config not configured with Solar PV / Grid topic.

Solar PV Divert requires the SolarPV-gen or Grid (+I/-E) MQTT topic to be defined on the Services tab.
Dynamically adjust charge rate based on solar PV generation or excess power (grid export).

1. Normal (default):

  • Charge at maximum current set by EVSE.
2. Eco (solar PV divert):
  • If only solar PV feed available: charge rate is modulated based on solar PV generation.
  • If grid +I/-E (positive Import / negative Export) feed is available: charge rate will be modulated by available excess power.
  • If EVSE is sleeping: charging will begin when solar PV / excess power > min charge rate.
  • Charging will not pause; this avoids excess wear on the EVSE relay and the EV.
Note: It's assumed that EVSE power is included in the grid feed

Charging mode can also be set via MQTT:
{base-topic}/divertmode/set

Normal (max) Eco (PV divert)

Solar: Grid Import/Export: W - | Charge rate: A

Sensor Values

Sensor Value
Pilot:
Current Now:
Temp1:
Temp2:
Temp3:

Setup

Time: No RTC detected

Time:

Manual Automatic

Service Level:

Time zone:

Set time from

Service Level:

Max Current:

Max Current:

Current

Name
Service Level:
Level Minimum:
Level Maximum:
Sensor Scale:
Sensor Offset:

Safety

Hardware safety checks. Enable dev mode (System > Developer Mode) to enable/disable or use the physical LCD + menu button.

Warning!!

Not all the safety tests are enabled, please take extra care before charging your vehicle.
Test Status
GFI Self Test:
Ground Monitoring:
Stuck Contact Detection:
Temperature Monitoring:
Diode Check:
Vent Required:
Error Count
GFCI:
No Ground:
Stuck Contact:

Hardware

OpenEVSE
Firmware:
Protocol:
OpenEVSE WiFi
Firmware:
Flash Size:
Free RAM:

Display

Simple Advanced

RAPI Command:

RAPI System Functions

Function Description
$FB LCD Backlight Color (0-7)
$FD Disable EVSE
$FE Enable EVSE
$FP Output text at x y position text to LCD (x y text)
$FR Reset EVSE
$FS Put EVSE to sleep

RAPI Get Commands

Get Description
$G3 Get Charge Time Limit, 15-minutes (1), 30-minutes (2), etc...
$GA Get Ammeter Scale/Offset, scale offset
$GC Get current capacity range, integers
$GE Get settings, amps flags
$GF Get fault counters, gfi ground stuck (in hex)
$GG Get charge current and voltage, milliamps millivolts
$GH Get charge limit in kWh, integer
$GM Get Voltmeter Scale/Offset, scale offset
$GO Get Overtemperature thresholds, ambient infrared
$GP Get Temperatures, LCD mcp9808 infrared (all integers, Celsius * 10)
$GS Get EVSE State, state elapsed_seconds
$GT Get time, year month day hour minute second
$GU Get Energy usage, wattseconds watt_hour_accumulated
$GV Get Versions, evse_firmware protocol_version

RAPI Set Commands

Set Description
$S0 Set LCD Type, Monochrome (0), Color (1)
$S1 Set RTC Year Month Day Hour Minute Second (all 2-digit max)
$S2 Enable (1)/ Disable (0) Ammeter Calibration Mode
$S3 Set Charge Time Limit, 15-minutes (1), 30-minutes (2), etc...
$SA Set Ammeter Scale/Offset, scale offset
$SC Set current capacity, integer
$SDEnable (1) / Disable (0) Diode self-check
$SF Enable (1) / Disable (0) GFI self-check
$SG Enable (1) / Disable (0) Ground check
$SH Set charge limit in kWh, integer
$SK Set accumulated Wh, integer
$SLSet service level (1/2/A)
$SM Set Voltmeter Scale/Offset, scale offset
$SO Set Overtemperature thresholds, ambient infrared
$SR Enable (1) / Disable (0) Stuck-relay check
$SS Enable (1) / Disable (0) GFI self-check
$ST Set timer, start_hour start_min end_hour end_min
$SV Enable (1) / Disable (0) vent required check

Powered by OpenEVSE and OpenEnergyMonitor
Version: V
\n"; + " value: openevse.currentCapacity,\n" + " disable: openevse.updatingServiceLevel() || openevse.updatingCurrentCapacity(),\n" + " css: { saved: openevse.savedCurrentCapacity }\">

Current

Name
Service Level:
Level Minimum:
Level Maximum:
Sensor Scale:
Sensor Offset:

Safety

Hardware safety checks. Enable dev mode (System > Developer Mode) to enable/disable or use the physical LCD + menu button.

Warning!!

Not all the safety tests are enabled, please take extra care before charging your vehicle.
Test Status
GFI Self Test:
Ground Monitoring:
Stuck Contact Detection:
Temperature Monitoring:
Diode Check:
Vent Required:
Error Count
GFCI:
No Ground:
Stuck Contact:

Hardware

OpenEVSE
Firmware:
Protocol:
OpenEVSE WiFi
Firmware:
Flash Size:
Free RAM:

Display

Simple Advanced

RAPI Command:

RAPI System Functions

Function Description
$FB LCD Backlight Color (0-7)
$FD Disable EVSE
$FE Enable EVSE
$FP Output text at x y position text to LCD (x y text)
$FR Reset EVSE
$FS Put EVSE to sleep

RAPI Get Commands

Get Description
$G3 Get Charge Time Limit, 15-minutes (1), 30-minutes (2), etc...
$GA Get Ammeter Scale/Offset, scale offset
$GC Get current capacity range, integers
$GD Get delay timer, starthr startmin endhr endmin
$GE Get settings, amps flags
$GF Get fault counters, gfi ground stuck (in hex)
$GG Get charge current and voltage, milliamps millivolts
$GH Get charge limit in kWh, integer
$GM Get Voltmeter Scale/Offset, scale offset
$GO Get Overtemperature thresholds, ambient infrared
$GP Get Temperatures, LCD mcp9808 infrared (all integers, Celsius * 10)
$GS Get EVSE State, state elapsed_seconds
$GT Get time, year month day hour minute second
$GU Get Energy usage, wattseconds watt_hour_accumulated
$GV Get Versions, evse_firmware protocol_version

RAPI Set Commands

Set Description
$S0 Set LCD Type, Monochrome (0), Color (1)
$S1 Set RTC Year Month Day Hour Minute Second (all 2-digit max)
$S2 Enable (1)/ Disable (0) Ammeter Calibration Mode
$S3 Set Charge Time Limit, 15-minutes (1), 30-minutes (2), etc...
$SA Set Ammeter Scale/Offset, scale offset
$SC Set current capacity, integer
$SDEnable (1) / Disable (0) Diode self-check
$SF Enable (1) / Disable (0) GFI self-check
$SG Enable (1) / Disable (0) Ground check
$SH Set charge limit in kWh, integer
$SK Set accumulated Wh, integer
$SLSet service level (1/2/A)
$SM Set Voltmeter Scale/Offset, scale offset
$SO Set Overtemperature thresholds, ambient infrared
$SR Enable (1) / Disable (0) Stuck-relay check
$SS Enable (1) / Disable (0) GFI self-check
$ST Set timer, start_hour start_min end_hour end_min
$SV Enable (1) / Disable (0) vent required check

Powered by OpenEVSE and OpenEnergyMonitor
Version: V
\n"; diff --git a/src/web_static/web_server.home.js.h b/src/web_static/web_server.home.js.h index 4af32c5..0343d96 100644 --- a/src/web_static/web_server.home.js.h +++ b/src/web_static/web_server.home.js.h @@ -1,3 +1,3 @@ static const char CONTENT_HOME_JS[] PROGMEM = - "\"use strict\";function OpenEVSEError(e){var n=1\"+e.ret),n.cmd(e.cmd)},\"json\").always(function(){n.rapiSend(!1)})}}function TimeViewModel(t){var a=this;function r(e){return(e<10?\"0\":\"\")+e}a.evseTimedate=ko.observable(new Date),a.localTimedate=ko.observable(new Date),a.nowTimedate=ko.observable(null),a.hasRTC=ko.observable(!0),a.elapsedNow=ko.observable(new Date(0)),a.elapsedLocal=ko.observable(new Date),a.divertUpdateNow=ko.observable(new Date(0)),a.divertUpdateLocal=ko.observable(new Date),a.date=ko.pureComputed({read:function(){if(null===a.nowTimedate())return\"\";var e=a.nowTimedate();return e.getFullYear()+\"-\"+r(e.getMonth()+1)+\"-\"+r(e.getDate())},write:function(e){a.evseTimedate(new Date(e)),a.localTimedate(new Date)}}),a.time=ko.pureComputed({read:function(){if(null===a.nowTimedate())return\"--:--:--\";var e=a.nowTimedate();return r(e.getHours())+\":\"+r(e.getMinutes())+\":\"+r(e.getSeconds())},write:function(e){var n=e.split(\":\"),t=a.evseTimedate();t.setHours(parseInt(n[0])),t.setMinutes(parseInt(n[1])),a.evseTimedate(t),a.localTimedate(new Date)}}),a.elapsed=ko.pureComputed(function(){if(null===a.nowTimedate())return\"0:00:00\";var e=a.elapsedNow().getTime(),n=(e=Math.floor(e/1e3))%60,t=(e=Math.floor(e/60))%60;return Math.floor(e/60)+\":\"+r(t)+\":\"+r(n)}),t.status.elapsed.subscribe(function(e){a.elapsedNow(new Date(1e3*e)),a.elapsedLocal(new Date)}),a.divert_update=ko.pureComputed(function(){if(null===a.nowTimedate())return!1;var e=a.divertUpdateNow().getTime();return Math.floor(e/1e3)}),t.status.divert_update.subscribe(function(e){a.divertUpdateNow(new Date(1e3*e)),a.divertUpdateLocal(new Date)});var o=null;a.automaticTime=ko.observable(!0),a.setTime=function(){var e=a.automaticTime()?new Date:a.evseTimedate();t.openevse.time(a.timeUpdate,e)},a.timeUpdate=function(e){var n=!(1=e){r.timeLimit(t.value);break}}},r.selectChargeLimit=function(e){if(r.chargeLimit()!==e)for(var n=0;n=e){r.chargeLimit(t.value);break}}};var a=[function(){return r.openevse.time(r.time.timeUpdate)},function(){return r.openevse.service_level(function(e,n){r.serviceLevel(e),r.actualServiceLevel(n)})},function(){return r.updateCurrentCapacity()},function(){return r.openevse.current_capacity(function(e){r.currentCapacity(e)})},function(){return r.openevse.time_limit(function(e){r.selectTimeLimit(e)})},function(){return r.openevse.charge_limit(function(e){r.selectChargeLimit(e)})},function(){return r.openevse.gfi_self_test(function(e){r.gfiSelfTestEnabled(e)})},function(){return r.openevse.ground_check(function(e){r.groundCheckEnabled(e)})},function(){return r.openevse.stuck_relay_check(function(e){r.stuckRelayEnabled(e)})},function(){return r.openevse.temp_check(function(e){r.tempCheckEnabled(e)})},function(){return r.openevse.diode_check(function(e){r.diodeCheckEnabled(e)})},function(){return r.openevse.vent_required(function(e){r.ventRequiredEnabled(e)})},function(){return r.openevse.temp_check(function(){r.tempCheckSupported(!0)},r.tempCheckEnabled()).error(function(){r.tempCheckSupported(!1)})},function(){return r.openevse.timer(function(e,n,t){r.delayTimerEnabled(e),r.delayTimerStart(n),r.delayTimerStop(t)})}];r.updateCount=ko.observable(0),r.updateTotal=ko.observable(a.length),r.updateCurrentCapacity=function(){return r.openevse.current_capacity_range(function(e,n){r.minCurrentLevel(e),r.maxCurrentLevel(n);var t=r.currentCapacity();r.currentLevels.removeAll();for(var a=r.minCurrentLevel();a<=r.maxCurrentLevel();a++)r.currentLevels.push({name:a+\" A\",value:a});r.currentCapacity(t)})},r.updatingServiceLevel=ko.observable(!1),r.savedServiceLevel=ko.observable(!1),r.updatingCurrentCapacity=ko.observable(!1),r.savedCurrentCapacity=ko.observable(!1),r.updatingTimeLimit=ko.observable(!1),r.savedTimeLimit=ko.observable(!1),r.updatingChargeLimit=ko.observable(!1),r.savedChargeLimit=ko.observable(!1),r.updatingDelayTimer=ko.observable(!1),r.savedDelayTimer=ko.observable(!1),r.updatingStatus=ko.observable(!1),r.savedStatus=ko.observable(!1),r.updatingGfiSelfTestEnabled=ko.observable(!1),r.savedGfiSelfTestEnabled=ko.observable(!1),r.updatingGroundCheckEnabled=ko.observable(!1),r.savedGroundCheckEnabled=ko.observable(!1),r.updatingStuckRelayEnabled=ko.observable(!1),r.savedStuckRelayEnabled=ko.observable(!1),r.updatingTempCheckEnabled=ko.observable(!1),r.savedTempCheckEnabled=ko.observable(!1),r.updatingDiodeCheckEnabled=ko.observable(!1),r.savedDiodeCheckEnabled=ko.observable(!1),r.updatingVentRequiredEnabled=ko.observable(!1),r.savedVentRequiredEnabled=ko.observable(!1);var o=!(r.setForTime=function(e,n){e(!0),setTimeout(function(){e(!1)},n)});function i(e){return/([01]\\d|2[0-3]):([0-5]\\d)/.test(e)}r.subscribe=function(){o||(r.serviceLevel.subscribe(function(e){r.updatingServiceLevel(!0),r.openevse.service_level(function(e,n){r.setForTime(r.savedServiceLevel,2e3),r.actualServiceLevel(n),r.updateCurrentCapacity().always(function(){})},e).always(function(){r.updatingServiceLevel(!1)})}),r.currentCapacity.subscribe(function(n){!0!==r.updatingServiceLevel()&&(r.updatingCurrentCapacity(!0),r.openevse.current_capacity(function(e){r.setForTime(r.savedCurrentCapacity,2e3),n!==e&&r.currentCapacity(e)},n).always(function(){r.updatingCurrentCapacity(!1)}))}),r.timeLimit.subscribe(function(n){r.updatingTimeLimit(!0),r.openevse.time_limit(function(e){r.setForTime(r.savedTimeLimit,2e3),n!==e&&r.selectTimeLimit(e)},n).always(function(){r.updatingTimeLimit(!1)})}),r.chargeLimit.subscribe(function(n){r.updatingChargeLimit(!0),r.openevse.charge_limit(function(e){r.setForTime(r.savedChargeLimit,2e3),n!==e&&r.selectChargeLimit(e)},n).always(function(){r.updatingChargeLimit(!1)})}),r.gfiSelfTestEnabled.subscribe(function(n){r.updatingGfiSelfTestEnabled(!0),r.openevse.gfi_self_test(function(e){r.setForTime(r.savedGfiSelfTestEnabled,2e3),n!==e&&r.gfiSelfTestEnabled(e)},n).always(function(){r.updatingGfiSelfTestEnabled(!1)})}),r.groundCheckEnabled.subscribe(function(n){r.updatingGroundCheckEnabled(!0),r.openevse.ground_check(function(e){r.setForTime(r.savedGroundCheckEnabled,2e3),n!==e&&r.groundCheckEnabled(e)},n).always(function(){r.updatingGroundCheckEnabled(!1)})}),r.stuckRelayEnabled.subscribe(function(n){r.updatingStuckRelayEnabled(!0),r.savedStuckRelayEnabled(!1),r.openevse.stuck_relay_check(function(e){r.savedStuckRelayEnabled(!0),setTimeout(function(){r.savedStuckRelayEnabled(!1)},2e3),n!==e&&r.stuckRelayEnabled(e)},n).always(function(){r.updatingStuckRelayEnabled(!1)})}),r.tempCheckEnabled.subscribe(function(n){r.updatingTempCheckEnabled(!0),r.openevse.temp_check(function(e){r.setForTime(r.savedTempCheckEnabled,2e3),n!==e&&r.tempCheckEnabled(e)},n).always(function(){r.updatingTempCheckEnabled(!1)})}),r.diodeCheckEnabled.subscribe(function(n){r.updatingDiodeCheckEnabled(!0),r.openevse.diode_check(function(e){r.setForTime(r.savedDiodeCheckEnabled,2e3),n!==e&&r.diodeCheckEnabled(e)},n).always(function(){r.updatingDiodeCheckEnabled(!1)})}),r.ventRequiredEnabled.subscribe(function(n){r.updatingVentRequiredEnabled(!0),r.openevse.vent_required(function(e){r.setForTime(r.savedVentRequiredEnabled,2e3),n!==e&&r.ventRequiredEnabled(e)},n).always(function(){r.updatingVentRequiredEnabled(!1)})}),o=!0)},r.update=function(){var e=0\"+e.ret),t.cmd(e.cmd)},\"json\").always(function(){t.rapiSend(!1)})}}function TimeViewModel(n){var o=this;function r(e){return(e<10?\"0\":\"\")+e}o.evseTimedate=ko.observable(new Date),o.localTimedate=ko.observable(new Date),o.nowTimedate=ko.observable(null),o.hasRTC=ko.observable(!0),o.elapsedNow=ko.observable(new Date(0)),o.elapsedLocal=ko.observable(new Date),o.divertUpdateNow=ko.observable(new Date(0)),o.divertUpdateLocal=ko.observable(new Date),o.date=ko.pureComputed({read:function(){if(null===o.nowTimedate())return\"\";var e=o.nowTimedate();return e.getFullYear()+\"-\"+r(e.getMonth()+1)+\"-\"+r(e.getDate())},write:function(e){var t=o.evseTimedate();e+=\" \"+r(t.getHours())+\":\"+r(t.getMinutes())+\":\"+r(t.getSeconds()),o.evseTimedate(new Date(e)),o.localTimedate(new Date)}}),o.time=ko.pureComputed({read:function(){if(null===o.nowTimedate())return\"--:--:--\";var e=o.nowTimedate();return r(e.getHours())+\":\"+r(e.getMinutes())+\":\"+r(e.getSeconds())},write:function(e){var t=e.split(\":\"),n=o.evseTimedate();n.setHours(parseInt(t[0])),n.setMinutes(parseInt(t[1])),o.evseTimedate(n),o.localTimedate(new Date)}}),o.elapsed=ko.pureComputed(function(){if(null===o.nowTimedate())return\"0:00:00\";var e=o.elapsedNow().getTime(),t=(e=Math.floor(e/1e3))%60,n=(e=Math.floor(e/60))%60;return Math.floor(e/60)+\":\"+r(n)+\":\"+r(t)}),n.status.elapsed.subscribe(function(e){o.elapsedNow(new Date(1e3*e)),o.elapsedLocal(new Date)}),o.divert_update=ko.pureComputed(function(){if(null===o.nowTimedate())return!1;var e=o.divertUpdateNow().getTime();return Math.floor(e/1e3)}),n.status.divert_update.subscribe(function(e){o.divertUpdateNow(new Date(1e3*e)),o.divertUpdateLocal(new Date)});var a=null;o.automaticTime=ko.observable(!0),o.timeUpdate=function(e){var t=!(1=e){r.timeLimit(n.value);break}}},r.selectChargeLimit=function(e){if(r.chargeLimit()!==e)for(var t=0;t=e){r.chargeLimit(n.value);break}}};var o=[function(){return!1===r.status.time()?r.openevse.time(r.time.timeUpdate):new DummyRequest},function(){return r.openevse.service_level(function(e,t){r.serviceLevel(e),r.actualServiceLevel(t)})},function(){return r.updateCurrentCapacity()},function(){return r.openevse.current_capacity(function(e){r.currentCapacity(e)})},function(){return r.openevse.time_limit(function(e){r.selectTimeLimit(e)})},function(){return r.openevse.charge_limit(function(e){r.selectChargeLimit(e)})},function(){return r.openevse.gfi_self_test(function(e){r.gfiSelfTestEnabled(e)})},function(){return r.openevse.ground_check(function(e){r.groundCheckEnabled(e)})},function(){return r.openevse.stuck_relay_check(function(e){r.stuckRelayEnabled(e)})},function(){return r.openevse.temp_check(function(e){r.tempCheckEnabled(e)})},function(){return r.openevse.diode_check(function(e){r.diodeCheckEnabled(e)})},function(){return r.openevse.vent_required(function(e){r.ventRequiredEnabled(e)})},function(){return r.openevse.temp_check(function(){r.tempCheckSupported(!0)},r.tempCheckEnabled()).error(function(){r.tempCheckSupported(!1)})},function(){return r.openevse.timer(function(e,t,n){r.delayTimerEnabled(e),r.delayTimerStart(t),r.delayTimerStop(n)})}];r.updateCount=ko.observable(0),r.updateTotal=ko.observable(o.length),r.updateCurrentCapacity=function(){return r.openevse.current_capacity_range(function(e,t){r.minCurrentLevel(e),r.maxCurrentLevel(t);var n=r.currentCapacity();r.currentLevels.removeAll();for(var o=r.minCurrentLevel();o<=r.maxCurrentLevel();o++)r.currentLevels.push({name:o+\" A\",value:o});r.currentCapacity(n)})},r.updatingServiceLevel=ko.observable(!1),r.savedServiceLevel=ko.observable(!1),r.updatingCurrentCapacity=ko.observable(!1),r.savedCurrentCapacity=ko.observable(!1),r.updatingTimeLimit=ko.observable(!1),r.savedTimeLimit=ko.observable(!1),r.updatingChargeLimit=ko.observable(!1),r.savedChargeLimit=ko.observable(!1),r.updatingDelayTimer=ko.observable(!1),r.savedDelayTimer=ko.observable(!1),r.updatingStatus=ko.observable(!1),r.savedStatus=ko.observable(!1),r.updatingGfiSelfTestEnabled=ko.observable(!1),r.savedGfiSelfTestEnabled=ko.observable(!1),r.updatingGroundCheckEnabled=ko.observable(!1),r.savedGroundCheckEnabled=ko.observable(!1),r.updatingStuckRelayEnabled=ko.observable(!1),r.savedStuckRelayEnabled=ko.observable(!1),r.updatingTempCheckEnabled=ko.observable(!1),r.savedTempCheckEnabled=ko.observable(!1),r.updatingDiodeCheckEnabled=ko.observable(!1),r.savedDiodeCheckEnabled=ko.observable(!1),r.updatingVentRequiredEnabled=ko.observable(!1),r.savedVentRequiredEnabled=ko.observable(!1);var a=!(r.setForTime=function(e,t){e(!0),setTimeout(function(){e(!1)},t)});function i(e){return/([01]\\d|2[0-3]):([0-5]\\d)/.test(e)}r.subscribe=function(){a||(r.serviceLevel.subscribe(function(e){r.updatingServiceLevel(!0),r.openevse.service_level(function(e,t){r.setForTime(r.savedServiceLevel,2e3),r.actualServiceLevel(t),r.updateCurrentCapacity().always(function(){})},e).always(function(){r.updatingServiceLevel(!1)})}),r.currentCapacity.subscribe(function(t){!0!==r.updatingServiceLevel()&&(r.updatingCurrentCapacity(!0),r.openevse.current_capacity(function(e){r.setForTime(r.savedCurrentCapacity,2e3),t!==e&&r.currentCapacity(e)},t).always(function(){r.updatingCurrentCapacity(!1)}))}),r.timeLimit.subscribe(function(t){r.updatingTimeLimit(!0),r.openevse.time_limit(function(e){r.setForTime(r.savedTimeLimit,2e3),t!==e&&r.selectTimeLimit(e)},t).always(function(){r.updatingTimeLimit(!1)})}),r.chargeLimit.subscribe(function(t){r.updatingChargeLimit(!0),r.openevse.charge_limit(function(e){r.setForTime(r.savedChargeLimit,2e3),t!==e&&r.selectChargeLimit(e)},t).always(function(){r.updatingChargeLimit(!1)})}),r.gfiSelfTestEnabled.subscribe(function(t){r.updatingGfiSelfTestEnabled(!0),r.openevse.gfi_self_test(function(e){r.setForTime(r.savedGfiSelfTestEnabled,2e3),t!==e&&r.gfiSelfTestEnabled(e)},t).always(function(){r.updatingGfiSelfTestEnabled(!1)})}),r.groundCheckEnabled.subscribe(function(t){r.updatingGroundCheckEnabled(!0),r.openevse.ground_check(function(e){r.setForTime(r.savedGroundCheckEnabled,2e3),t!==e&&r.groundCheckEnabled(e)},t).always(function(){r.updatingGroundCheckEnabled(!1)})}),r.stuckRelayEnabled.subscribe(function(t){r.updatingStuckRelayEnabled(!0),r.savedStuckRelayEnabled(!1),r.openevse.stuck_relay_check(function(e){r.savedStuckRelayEnabled(!0),setTimeout(function(){r.savedStuckRelayEnabled(!1)},2e3),t!==e&&r.stuckRelayEnabled(e)},t).always(function(){r.updatingStuckRelayEnabled(!1)})}),r.tempCheckEnabled.subscribe(function(t){r.updatingTempCheckEnabled(!0),r.openevse.temp_check(function(e){r.setForTime(r.savedTempCheckEnabled,2e3),t!==e&&r.tempCheckEnabled(e)},t).always(function(){r.updatingTempCheckEnabled(!1)})}),r.diodeCheckEnabled.subscribe(function(t){r.updatingDiodeCheckEnabled(!0),r.openevse.diode_check(function(e){r.setForTime(r.savedDiodeCheckEnabled,2e3),t!==e&&r.diodeCheckEnabled(e)},t).always(function(){r.updatingDiodeCheckEnabled(!1)})}),r.ventRequiredEnabled.subscribe(function(t){r.updatingVentRequiredEnabled(!0),r.openevse.vent_required(function(e){r.setForTime(r.savedVentRequiredEnabled,2e3),t!==e&&r.ventRequiredEnabled(e)},t).always(function(){r.updatingVentRequiredEnabled(!1)})}),a=!0)},r.update=function(){var e=0+~]|\"+q+\")\"+q+\"*\"),V=new RegExp(\"=\"+q+\"*([^\\\\]'\\\"]*?)\"+q+\"*\\\\]\",\"g\"),U=new RegExp(I),J=new RegExp(\"^\"+B+\"$\"),z={ID:new RegExp(\"^#(\"+B+\")\"),CLASS:new RegExp(\"^\\\\.(\"+B+\")\"),TAG:new RegExp(\"^(\"+B+\"|[*])\"),ATTR:new RegExp(\"^\"+H),PSEUDO:new RegExp(\"^\"+I),CHILD:new RegExp(\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\"+q+\"*(even|odd|(([+-]|)(\\\\d*)n|)\"+q+\"*(?:([+-]|)\"+q+\"*(\\\\d+)|))\"+q+\"*\\\\)|)\",\"i\"),bool:new RegExp(\"^(?:\"+P+\")$\",\"i\"),needsContext:new RegExp(\"^\"+q+\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\"+q+\"*((?:-\\\\d)?\\\\d*)\"+q+\"*\\\\)|)(?=[^-]|$)\",\"i\")},G=/^(?:input|select|textarea|button)$/i,X=/^h\\d$/i,Y=/^[^{]+\\{\\s*\\[native \\w/,K=/^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,Q=/[+~]/,Z=new RegExp(\"\\\\\\\\([\\\\da-f]{1,6}\"+q+\"?|(\"+q+\")|.)\",\"ig\"),ee=function(e,t,n){var r=\"0x\"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,ne=function(e,t){return t?\"\\0\"===e?\"�\":e.slice(0,-1)+\"\\\\\"+e.charCodeAt(e.length-1).toString(16)+\" \":\"\\\\\"+e},re=function(){C()},ie=be(function(e){return!0===e.disabled&&(\"form\"in e||\"label\"in e)},{dir:\"parentNode\",next:\"legend\"});try{_.apply(t=L.call(b.childNodes),b.childNodes),t[b.childNodes.length].nodeType}catch(e){_={apply:t.length?function(e,t){j.apply(e,L.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function oe(e,t,n,r){var i,o,a,s,u,c,l,f=t&&t.ownerDocument,d=t?t.nodeType:9;if(n=n||[],\"string\"!=typeof e||!e||1!==d&&9!==d&&11!==d)return n;if(!r&&((t?t.ownerDocument||t:b)!==T&&C(t),t=t||T,k)){if(11!==d&&(u=K.exec(e)))if(i=u[1]){if(9===d){if(!(a=t.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&m(t,a)&&a.id===i)return n.push(a),n}else{if(u[2])return _.apply(n,t.getElementsByTagName(e)),n;if((i=u[3])&&p.getElementsByClassName&&t.getElementsByClassName)return _.apply(n,t.getElementsByClassName(i)),n}if(p.qsa&&!N[e+\" \"]&&(!g||!g.test(e))){if(1!==d)f=t,l=e;else if(\"object\"!==t.nodeName.toLowerCase()){for((s=t.getAttribute(\"id\"))?s=s.replace(te,ne):t.setAttribute(\"id\",s=E),o=(c=h(e)).length;o--;)c[o]=\"#\"+s+\" \"+me(c[o]);l=c.join(\",\"),f=Q.test(e)&&ve(t.parentNode)||t}if(l)try{return _.apply(n,f.querySelectorAll(l)),n}catch(e){}finally{s===E&&t.removeAttribute(\"id\")}}}return v(e.replace(F,\"$1\"),t,n,r)}function ae(){var r=[];return function e(t,n){return r.push(t+\" \")>w.cacheLength&&delete e[r.shift()],e[t+\" \"]=n}}function se(e){return e[E]=!0,e}function ue(e){var t=T.createElement(\"fieldset\");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ce(e,t){for(var n=e.split(\"|\"),r=n.length;r--;)w.attrHandle[n[r]]=t}function le(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function fe(t){return function(e){return\"input\"===e.nodeName.toLowerCase()&&e.type===t}}function de(n){return function(e){var t=e.nodeName.toLowerCase();return(\"input\"===t||\"button\"===t)&&e.type===n}}function pe(t){return function(e){return\"form\"in e?e.parentNode&&!1===e.disabled?\"label\"in e?\"label\"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ie(e)===t:e.disabled===t:\"label\"in e&&e.disabled===t}}function he(a){return se(function(o){return o=+o,se(function(e,t){for(var n,r=a([],e.length,o),i=r.length;i--;)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in p=oe.support={},i=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&\"HTML\"!==t.nodeName},C=oe.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:b;return r!==T&&9===r.nodeType&&r.documentElement&&(a=(T=r).documentElement,k=!i(T),b!==T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener(\"unload\",re,!1):n.attachEvent&&n.attachEvent(\"onunload\",re)),p.attributes=ue(function(e){return e.className=\"i\",!e.getAttribute(\"className\")}),p.getElementsByTagName=ue(function(e){return e.appendChild(T.createComment(\"\")),!e.getElementsByTagName(\"*\").length}),p.getElementsByClassName=Y.test(T.getElementsByClassName),p.getById=ue(function(e){return a.appendChild(e).id=E,!T.getElementsByName||!T.getElementsByName(E).length}),p.getById?(w.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute(\"id\")===t}},w.find.ID=function(e,t){if(void 0!==t.getElementById&&k){var n=t.getElementById(e);return n?[n]:[]}}):(w.filter.ID=function(e){var n=e.replace(Z,ee);return function(e){var t=void 0!==e.getAttributeNode&&e.getAttributeNode(\"id\");return t&&t.value===n}},w.find.ID=function(e,t){if(void 0!==t.getElementById&&k){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode(\"id\"))&&n.value===e)return[o];for(i=t.getElementsByName(e),r=0;o=i[r++];)if((n=o.getAttributeNode(\"id\"))&&n.value===e)return[o]}return[]}}),w.find.TAG=p.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if(\"*\"!==e)return o;for(;n=o[i++];)1===n.nodeType&&r.push(n);return r},w.find.CLASS=p.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&k)return t.getElementsByClassName(e)},s=[],g=[],(p.qsa=Y.test(T.querySelectorAll))&&(ue(function(e){a.appendChild(e).innerHTML=\"\",e.querySelectorAll(\"[msallowcapture^='']\").length&&g.push(\"[*^$]=\"+q+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\"[selected]\").length||g.push(\"\\\\[\"+q+\"*(?:value|\"+P+\")\"),e.querySelectorAll(\"[id~=\"+E+\"-]\").length||g.push(\"~=\"),e.querySelectorAll(\":checked\").length||g.push(\":checked\"),e.querySelectorAll(\"a#\"+E+\"+*\").length||g.push(\".#.+[+~]\")}),ue(function(e){e.innerHTML=\"\";var t=T.createElement(\"input\");t.setAttribute(\"type\",\"hidden\"),e.appendChild(t).setAttribute(\"name\",\"D\"),e.querySelectorAll(\"[name=d]\").length&&g.push(\"name\"+q+\"*[*^$|!~]?=\"),2!==e.querySelectorAll(\":enabled\").length&&g.push(\":enabled\",\":disabled\"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(\":disabled\").length&&g.push(\":enabled\",\":disabled\"),e.querySelectorAll(\"*,:x\"),g.push(\",.*:\")})),(p.matchesSelector=Y.test(l=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ue(function(e){p.disconnectedMatch=l.call(e,\"*\"),l.call(e,\"[s!='']:x\"),s.push(\"!=\",I)}),g=g.length&&new RegExp(g.join(\"|\")),s=s.length&&new RegExp(s.join(\"|\")),t=Y.test(a.compareDocumentPosition),m=t||Y.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return c=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument===b&&m(b,e)?-1:t===T||t.ownerDocument===b&&m(b,t)?1:u?M(u,e)-M(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return c=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===T?-1:t===T?1:i?-1:o?1:u?M(u,e)-M(u,t):0;if(i===o)return le(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?le(a[r],s[r]):a[r]===b?-1:s[r]===b?1:0}),T},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==T&&C(e),t=t.replace(V,\"='$1']\"),p.matchesSelector&&k&&!N[t+\" \"]&&(!s||!s.test(t))&&(!g||!g.test(t)))try{var n=l.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){}return 0\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||\"\").replace(Z,ee),\"~=\"===e[2]&&(e[3]=\" \"+e[3]+\" \"),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),\"nth\"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*(\"even\"===e[3]||\"odd\"===e[3])),e[5]=+(e[7]+e[8]||\"odd\"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return z.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||\"\":n&&U.test(n)&&(t=h(n,!0))&&(t=n.indexOf(\")\",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return\"*\"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=d[e+\" \"];return t||(t=new RegExp(\"(^|\"+q+\")\"+e+\"(\"+q+\"|$)\"))&&d(e,function(e){return t.test(\"string\"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute(\"class\")||\"\")})},ATTR:function(n,r,i){return function(e){var t=oe.attr(e,n);return null==t?\"!=\"===r:!r||(t+=\"\",\"=\"===r?t===i:\"!=\"===r?t!==i:\"^=\"===r?i&&0===t.indexOf(i):\"*=\"===r?i&&-1\",\"#\"===e.firstChild.getAttribute(\"href\")})||ce(\"type|href|height|width\",function(e,t,n){if(!n)return e.getAttribute(t,\"type\"===t.toLowerCase()?1:2)}),p.attributes&&ue(function(e){return e.innerHTML=\"\",e.firstChild.setAttribute(\"value\",\"\"),\"\"===e.firstChild.getAttribute(\"value\")})||ce(\"value\",function(e,t,n){if(!n&&\"input\"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute(\"disabled\")})||ce(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(T);E.find=h,E.expr=h.selectors,E.expr[\":\"]=E.expr.pseudos,E.uniqueSort=E.unique=h.uniqueSort,E.text=h.getText,E.isXMLDoc=h.isXML,E.contains=h.contains,E.escapeSelector=h.escape;var C=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&E(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},N=E.expr.match.needsContext;function D(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;function O(e,n,r){return b(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):\"string\"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,\"string\"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):b(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this);if(!(r=\"<\"===e[0]&&\">\"===e[e.length-1]&&3<=e.length?[null,e,null]:_.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:k,!0)),A.test(r[1])&&E.isPlainObject(t))for(r in t)b(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=k.getElementById(r[2]))&&(this[0]=i,this.length=1),this}).prototype=E.fn,j=E(k);var L=/^(?:parents|prev(?:Until|All))/,M={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\\x20\\t\\r\\n\\f]+)/i,fe=/^$|^module$|\\/(?:java|ecma)script/i,de={option:[1,\"\"],thead:[1,\"\",\"
\"],col:[2,\"\",\"
\"],tr:[2,\"\",\"
\"],td:[3,\"\",\"
\"],_default:[0,\"\",\"\"]};function pe(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||\"*\"):void 0!==e.querySelectorAll?e.querySelectorAll(t||\"*\"):[],void 0===t||t&&D(e,t)?E.merge([e],n):n}function he(e,t){for(var n=0,r=e.length;nx\",m.noCloneChecked=!!ve.cloneNode(!0).lastChild.defaultValue;var ye=k.documentElement,we=/^key/,xe=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\\.(.+)|)/;function Te(){return!0}function ke(){return!1}function Ee(){try{return k.activeElement}catch(e){}}function Se(e,t,n,r,i,o){var a,s;if(\"object\"===_typeof(t)){for(s in\"string\"!=typeof n&&(r=r||n,n=void 0),t)Se(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&(\"string\"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return E().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=E.guid++)),e.each(function(){E.event.add(this,t,i,r,n)})}E.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,c,l,f,d,p,h,v,g=Y.get(t);if(g)for(n.handler&&(n=(o=n).handler,i=o.selector),i&&E.find.matchesSelector(ye,i),n.guid||(n.guid=E.guid++),(u=g.events)||(u=g.events={}),(a=g.handle)||(a=g.handle=function(e){return void 0!==E&&E.event.triggered!==e.type?E.event.dispatch.apply(t,arguments):void 0}),c=(e=(e||\"\").match(q)||[\"\"]).length;c--;)p=v=(s=Ce.exec(e[c])||[])[1],h=(s[2]||\"\").split(\".\").sort(),p&&(f=E.event.special[p]||{},p=(i?f.delegateType:f.bindType)||p,f=E.event.special[p]||{},l=E.extend({type:p,origType:v,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&E.expr.match.needsContext.test(i),namespace:h.join(\".\")},o),(d=u[p])||((d=u[p]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(p,a)),f.add&&(f.add.call(t,l),l.handler.guid||(l.handler.guid=n.guid)),i?d.splice(d.delegateCount++,0,l):d.push(l),E.event.global[p]=!0)},remove:function(e,t,n,r,i){var o,a,s,u,c,l,f,d,p,h,v,g=Y.hasData(e)&&Y.get(e);if(g&&(u=g.events)){for(c=(t=(t||\"\").match(q)||[\"\"]).length;c--;)if(p=v=(s=Ce.exec(t[c])||[])[1],h=(s[2]||\"\").split(\".\").sort(),p){for(f=E.event.special[p]||{},d=u[p=(r?f.delegateType:f.bindType)||p]||[],s=s[2]&&new RegExp(\"(^|\\\\.)\"+h.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"),a=o=d.length;o--;)l=d[o],!i&&v!==l.origType||n&&n.guid!==l.guid||s&&!s.test(l.namespace)||r&&r!==l.selector&&(\"**\"!==r||!l.selector)||(d.splice(o,1),l.selector&&d.delegateCount--,f.remove&&f.remove.call(e,l));a&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,h,g.handle)||E.removeEvent(e,p,g.handle),delete u[p])}else for(p in u)E.event.remove(e,p+t[c],n,r,!0);E.isEmptyObject(u)&&Y.remove(e,\"handle events\")}},dispatch:function(e){var t,n,r,i,o,a,s=E.event.fix(e),u=new Array(arguments.length),c=(Y.get(this,\"events\")||{})[s.type]||[],l=E.event.special[s.type]||{};for(u[0]=s,t=1;t\\x20\\t\\r\\n\\f]*)[^>]*)\\/>/gi,De=/\\s*$/g;function je(e,t){return D(e,\"table\")&&D(11!==t.nodeType?t:t.firstChild,\"tr\")&&E(e).children(\"tbody\")[0]||e}function _e(e){return e.type=(null!==e.getAttribute(\"type\"))+\"/\"+e.type,e}function Le(e){return\"true/\"===(e.type||\"\").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute(\"type\"),e}function Me(e,t){var n,r,i,o,a,s,u,c;if(1===t.nodeType){if(Y.hasData(e)&&(o=Y.access(e),a=Y.set(t,o),c=o.events))for(i in delete a.handle,a.events={},c)for(n=0,r=c[i].length;n\")},clone:function(e,t,n){var r,i,o,a,s,u,c,l=e.cloneNode(!0),f=E.contains(e.ownerDocument,e);if(!(m.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||E.isXMLDoc(e)))for(a=pe(l),r=0,i=(o=pe(e)).length;r\").prop({charset:n.scriptCharset,src:n.url}).on(\"load error\",i=function(e){r.remove(),i=null,e&&t(\"error\"===e.type?404:200,e.type)}),k.head.appendChild(r[0])},abort:function(){i&&i()}}});var Wt,Vt=[],Ut=/(=)\\?(?=&|$)|\\?\\?/;E.ajaxSetup({jsonp:\"callback\",jsonpCallback:function(){var e=Vt.pop()||E.expando+\"_\"+xt++;return this[e]=!0,e}}),E.ajaxPrefilter(\"json jsonp\",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?\"url\":\"string\"==typeof e.data&&0===(e.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&Ut.test(e.data)&&\"data\");if(a||\"jsonp\"===e.dataTypes[0])return r=e.jsonpCallback=b(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,\"$1\"+r):!1!==e.jsonp&&(e.url+=(Ct.test(e.url)?\"&\":\"?\")+e.jsonp+\"=\"+r),e.converters[\"script json\"]=function(){return o||E.error(r+\" was not called\"),o[0]},e.dataTypes[0]=\"json\",i=T[r],T[r]=function(){o=arguments},n.always(function(){void 0===i?E(T).removeProp(r):T[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Vt.push(r)),o&&b(i)&&i(o[0]),o=i=void 0}),\"script\"}),m.createHTMLDocument=((Wt=k.implementation.createHTMLDocument(\"\").body).innerHTML=\"
\",2===Wt.childNodes.length),E.parseHTML=function(e,t,n){return\"string\"!=typeof e?[]:(\"boolean\"==typeof t&&(n=t,t=!1),t||(m.createHTMLDocument?((r=(t=k.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=k.location.href,t.head.appendChild(r)):t=k),o=!n&&[],(i=A.exec(e))?[t.createElement(i[1])]:(i=be([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(\" \");return-1\").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},E.each([\"ajaxStart\",\"ajaxStop\",\"ajaxComplete\",\"ajaxError\",\"ajaxSuccess\",\"ajaxSend\"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,c=E.css(e,\"position\"),l=E(e),f={};\"static\"===c&&(e.style.position=\"relative\"),s=l.offset(),o=E.css(e,\"top\"),u=E.css(e,\"left\"),i=(\"absolute\"===c||\"fixed\"===c)&&-1<(o+u).indexOf(\"auto\")?(a=(r=l.position()).top,r.left):(a=parseFloat(o)||0,parseFloat(u)||0),b(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),\"using\"in t?t.using.call(e,f):l.css(f)}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if(\"fixed\"===E.css(r,\"position\"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&\"static\"===E.css(e,\"position\");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,\"borderTopWidth\",!0),i.left+=E.css(e,\"borderLeftWidth\",!0))}return{top:t.top-i.top-E.css(r,\"marginTop\",!0),left:t.left-i.left-E.css(r,\"marginLeft\",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&\"static\"===E.css(e,\"position\");)e=e.offsetParent;return e||ye})}}),E.each({scrollLeft:\"pageXOffset\",scrollTop:\"pageYOffset\"},function(t,i){var o=\"pageYOffset\"===i;E.fn[t]=function(e){return W(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each([\"top\",\"left\"],function(e,n){E.cssHooks[n]=Fe(m.pixelPosition,function(e,t){if(t)return t=Re(e,n),Be.test(t)?E(e).position()[n]+\"px\":t})}),E.each({Height:\"height\",Width:\"width\"},function(a,s){E.each({padding:\"inner\"+a,content:s,\"\":\"outer\"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||\"boolean\"!=typeof e),i=r||(!0===e||!0===t?\"margin\":\"border\");return W(this,function(e,t,n){var r;return y(e)?0===o.indexOf(\"outer\")?e[\"inner\"+a]:e.document.documentElement[\"client\"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body[\"scroll\"+a],r[\"scroll\"+a],e.body[\"offset\"+a],r[\"offset\"+a],r[\"client\"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,n){E.fn[n]=function(e,t){return 0e.length)&&e.substring(0,t.length)===t},ud:function(e,t){if(e===t)return!0;if(11===e.nodeType)return!1;if(t.contains)return t.contains(1!==e.nodeType?e.parentNode:e);if(t.compareDocumentPosition)return 16==(16&t.compareDocumentPosition(e));for(;e&&e!=t;)e=e.parentNode;return!!e},Rb:function(e){return j.a.ud(e,e.ownerDocument.documentElement)},jd:function(e){return!!j.a.Lb(e,j.a.Rb)},P:function(e){return e&&e.tagName&&e.tagName.toLowerCase()},zc:function(e){return j.onError?function(){try{return e.apply(this,arguments)}catch(e){throw j.onError&&j.onError(e),e}}:e},setTimeout:function(n){function e(e,t){return n.apply(this,arguments)}return e.toString=function(){return n.toString()},e}(function(e,t){return setTimeout(j.a.zc(e),t)}),Fc:function(e){setTimeout(function(){throw j.onError&&j.onError(e),e},0)},H:function(t,e,n){var r=j.a.zc(n);if(n=c[e],j.options.useOnlyNativeEvents||n||!L)if(n||\"function\"!=typeof t.addEventListener){if(void 0===t.attachEvent)throw Error(\"Browser doesn't support addEventListener or attachEvent\");var i=function(e){r.call(t,e)},o=\"on\"+e;t.attachEvent(o,i),j.a.I.za(t,function(){t.detachEvent(o,i)})}else t.addEventListener(e,r,!1);else u||(u=\"function\"==typeof L(t).on?\"on\":\"bind\"),L(t)[u](e,r)},Fb:function(e,t){if(!e||!e.nodeType)throw Error(\"element must be a DOM node when calling triggerEvent\");var n;if(n=!(\"input\"!==j.a.P(e)||!e.type||\"click\"!=t.toLowerCase()||\"checkbox\"!=(n=e.type)&&\"radio\"!=n),j.options.useOnlyNativeEvents||!L||n)if(\"function\"==typeof A.createEvent){if(\"function\"!=typeof e.dispatchEvent)throw Error(\"The supplied element doesn't support dispatchEvent\");(n=A.createEvent(s[t]||\"HTMLEvents\")).initEvent(t,!0,!0,D,0,0,0,0,0,!1,!1,!1,!1,0,e),e.dispatchEvent(n)}else if(n&&e.click)e.click();else{if(void 0===e.fireEvent)throw Error(\"Browser doesn't support triggering events\");e.fireEvent(\"on\"+t)}else L(e).trigger(t)},c:function(e){return j.N(e)?e():e},$b:function(e){return j.N(e)?e.w():e},Eb:function(t,e,n){var r;e&&(\"object\"===_typeof(t.classList)?(r=t.classList[n?\"add\":\"remove\"],j.a.C(e.match(d),function(e){r.call(t.classList,e)})):\"string\"==typeof t.className.baseVal?i(t.className,\"baseVal\",e,n):i(t,\"className\",e,n))},Ab:function(e,t){var n=j.a.c(t);null!==n&&n!==_||(n=\"\");var r=j.h.firstChild(e);!r||3!=r.nodeType||j.h.nextSibling(r)?j.h.ua(e,[e.ownerDocument.createTextNode(n)]):r.data=n,j.a.zd(e)},Xc:function(e,t){if(e.name=t,l<=7)try{var n=e.name.replace(/[&<>'\"]/g,function(e){return\"&#\"+e.charCodeAt(0)+\";\"});e.mergeAttributes(A.createElement(\"\"),!1)}catch(e){}},zd:function(e){9<=l&&(e=1==e.nodeType?e:e.parentNode).style&&(e.style.zoom=e.style.zoom)},vd:function(e){if(l){var t=e.style.width;e.style.width=0,e.style.width=t}},Od:function(e,t){e=j.a.c(e),t=j.a.c(t);for(var n=[],r=e;r<=t;r++)n.push(r);return n},la:function(e){for(var t=[],n=0,r=e.length;n\",\"\"],tbody:t,tfoot:t,tr:[2,\"\",\"
\"],td:l=[3,\"\",\"
\"],th:l,option:f=[1,\"\"],optgroup:f},p=j.a.W<=8,j.a.ta=function(e,t){var n;if(L){if(L.parseHTML)n=L.parseHTML(e,t)||[];else if((n=L.clean([e],t))&&n[0]){for(var r=n[0];r.parentNode&&11!==r.parentNode.nodeType;)r=r.parentNode;r.parentNode&&r.parentNode.removeChild(r)}}else{(n=t)||(n=A),r=n.parentWindow||n.defaultView||D;var i,o=j.a.Cb(e).toLowerCase(),a=n.createElement(\"div\");for(o=(i=(o=o.match(/^(?:\\x3c!--.*?--\\x3e\\s*?)*?<([a-z]+)[\\s>]/))&&d[o[1]]||u)[0],i=\"ignored
\"+i[1]+e+i[2]+\"
\",\"function\"==typeof r.innerShiv?a.appendChild(r.innerShiv(i)):(p&&n.body.appendChild(a),a.innerHTML=i,p&&a.parentNode.removeChild(a));o--;)a=a.lastChild;n=j.a.la(a.lastChild.childNodes)}return n},j.a.Ld=function(e,t){var n=j.a.ta(e,t);return n.length&&n[0].parentElement||j.a.Xb(n)},j.a.dc=function(e,t){if(j.a.Sb(e),null!==(t=j.a.c(t))&&t!==_)if(\"string\"!=typeof t&&(t=t.toString()),L)L(e).html(t);else for(var n=j.a.ta(t,e.ownerDocument),r=0;r]*))?)*\\s+)data-bind\\s*=\\s*([\"'])([\\s\\S]*?)\\3/gi,n=/\\x3c!--\\s*ko\\b\\s*([\\s\\S]*?)\\s*--\\x3e/g;return{wd:function(e,t,n){t.isTemplateRewritten(e,n)||t.rewriteTemplate(e,function(e){return j.ic.Kd(e,t)},n)},Kd:function(e,o){return e.replace(t,function(e,t,n,r,i){return a(i,t,n,o)}).replace(n,function(e,t){return a(t,\"\\x3c!-- ko --\\x3e\",\"#comment\",o)})},ld:function(r,i){return j.aa.Wb(function(e,t){var n=e.nextSibling;n&&n.nodeName.toLowerCase()===i&&j.eb(n,r,t)})}}}(),j.b(\"__tr_ambtns\",j.ic.ld),function(){j.B={},j.B.D=function(e){if(this.D=e){var t=j.a.P(e);this.Db=\"script\"===t?1:\"textarea\"===t?2:\"template\"==t&&e.content&&11===e.content.nodeType?3:4}},j.B.D.prototype.text=function(){var e=1===this.Db?\"text\":2===this.Db?\"value\":\"innerHTML\";if(0==arguments.length)return this.D[e];var t=arguments[0];\"innerHTML\"===e?j.a.dc(this.D,t):this.D[e]=t};var t=j.a.g.Z()+\"_\";j.B.D.prototype.data=function(e){if(1===arguments.length)return j.a.g.get(this.D,t+e);j.a.g.set(this.D,t+e,arguments[1])};var r=j.a.g.Z();j.B.D.prototype.nodes=function(){var e=this.D;if(0==arguments.length){var t=j.a.g.get(e,r)||{},n=t.jb||(3===this.Db?e.content:4===this.Db?e:_);return n&&!t.hd||(t=this.text())&&(n=j.a.Ld(t,e.ownerDocument),this.text(\"\"),j.a.g.set(e,r,{jb:n,hd:!0})),n}j.a.g.set(e,r,{jb:arguments[0]})},j.B.ia=function(e){this.D=e},j.B.ia.prototype=new j.B.D,j.B.ia.prototype.constructor=j.B.ia,j.B.ia.prototype.text=function(){if(0==arguments.length){var e=j.a.g.get(this.D,r)||{};return e.jc===_&&e.jb&&(e.jc=e.jb.innerHTML),e.jc}j.a.g.set(this.D,r,{jc:arguments[0]})},j.b(\"templateSources\",j.B),j.b(\"templateSources.domElement\",j.B.D),j.b(\"templateSources.anonymousTemplate\",j.B.ia)}(),function(){function r(e,t,n){var r;for(t=j.h.nextSibling(t);e&&(r=e)!==t;)n(r,e=j.h.nextSibling(r))}function d(e,t){if(e.length){var i=e[0],o=e[e.length-1],n=i.parentNode,a=j.ga.instance,s=a.preprocessNode;if(s){if(r(i,o,function(e,t){var n=e.previousSibling,r=s.call(a,e);r&&(e===i&&(i=r[0]||t),e===o&&(o=r[r.length-1]||n))}),e.length=0,!i)return;i===o?e.push(i):(e.push(i,o),j.a.Ua(e,n))}r(i,o,function(e){1!==e.nodeType&&8!==e.nodeType||j.uc(t,e)}),r(i,o,function(e){1!==e.nodeType&&8!==e.nodeType||j.aa.bd(e,[t])}),j.a.Ua(e,n)}}function u(e){return e.nodeType?e:0\"+t+\"<\\/script>\")},0>10|55296,1023&n|56320))}function o(){C()}var e,p,w,i,a,h,d,v,x,u,c,C,T,s,k,g,l,m,b,E=\"sizzle\"+ +new Date,y=n.document,S=0,r=0,N=ue(),_=ue(),A=ue(),D=ue(),O=function(e,t){return e===t&&(c=!0),0},j={}.hasOwnProperty,t=[],M=t.pop,q=t.push,L=t.push,P=t.slice,R=function(e,t){for(var n=0,r=e.length;n+~]|\"+B+\")\"+B+\"*\"),z=new RegExp(B+\"|>\"),G=new RegExp($),X=new RegExp(\"^\"+H+\"$\"),Y={ID:new RegExp(\"^#(\"+H+\")\"),CLASS:new RegExp(\"^\\\\.(\"+H+\")\"),TAG:new RegExp(\"^(\"+H+\"|[*])\"),ATTR:new RegExp(\"^\"+F),PSEUDO:new RegExp(\"^\"+$),CHILD:new RegExp(\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\"+B+\"*(even|odd|(([+-]|)(\\\\d*)n|)\"+B+\"*(?:([+-]|)\"+B+\"*(\\\\d+)|))\"+B+\"*\\\\)|)\",\"i\"),bool:new RegExp(\"^(?:\"+I+\")$\",\"i\"),needsContext:new RegExp(\"^\"+B+\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\"+B+\"*((?:-\\\\d)?\\\\d*)\"+B+\"*\\\\)|)(?=[^-]|$)\",\"i\")},K=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,Z=/^h\\d$/i,ee=/^[^{]+\\{\\s*\\[native \\w/,te=/^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,ne=/[+~]/,re=new RegExp(\"\\\\\\\\[\\\\da-fA-F]{1,6}\"+B+\"?|\\\\\\\\([^\\\\r\\\\n\\\\f])\",\"g\"),oe=/([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,ie=function(e,t){return t?\"\\0\"===e?\"�\":e.slice(0,-1)+\"\\\\\"+e.charCodeAt(e.length-1).toString(16)+\" \":\"\\\\\"+e},ae=we(function(e){return!0===e.disabled&&\"fieldset\"===e.nodeName.toLowerCase()},{dir:\"parentNode\",next:\"legend\"});try{L.apply(t=P.call(y.childNodes),y.childNodes),t[y.childNodes.length].nodeType}catch(e){L={apply:t.length?function(e,t){q.apply(e,P.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function se(t,e,n,r){var o,i,a,s,u,c,l,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],\"string\"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(C(e),e=e||T,k)){if(11!==d&&(u=te.exec(t)))if(o=u[1]){if(9===d){if(!(a=e.getElementById(o)))return n;if(a.id===o)return n.push(a),n}else if(f&&(a=f.getElementById(o))&&b(e,a)&&a.id===o)return n.push(a),n}else{if(u[2])return L.apply(n,e.getElementsByTagName(t)),n;if((o=u[3])&&p.getElementsByClassName&&e.getElementsByClassName)return L.apply(n,e.getElementsByClassName(o)),n}if(p.qsa&&!D[t+\" \"]&&(!g||!g.test(t))&&(1!==d||\"object\"!==e.nodeName.toLowerCase())){if(l=t,f=e,1===d&&(z.test(t)||J.test(t))){for((f=ne.test(t)&&me(e.parentNode)||e)===e&&p.scope||((s=e.getAttribute(\"id\"))?s=s.replace(oe,ie):e.setAttribute(\"id\",s=E)),i=(c=h(t)).length;i--;)c[i]=(s?\"#\"+s:\":scope\")+\" \"+ye(c[i]);l=c.join(\",\")}try{return L.apply(n,f.querySelectorAll(l)),n}catch(e){D(t,!0)}finally{s===E&&e.removeAttribute(\"id\")}}}return v(t.replace(V,\"$1\"),e,n,r)}function ue(){var n=[];function r(e,t){return n.push(e+\" \")>w.cacheLength&&delete r[n.shift()],r[e+\" \"]=t}return r}function ce(e){return e[E]=!0,e}function le(e){var t=T.createElement(\"fieldset\");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){for(var n=e.split(\"|\"),r=n.length;r--;)w.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return\"input\"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return(\"input\"===t||\"button\"===t)&&e.type===n}}function ve(t){return function(e){return\"form\"in e?e.parentNode&&!1===e.disabled?\"label\"in e?\"label\"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:\"label\"in e&&e.disabled===t}}function ge(a){return ce(function(i){return i=+i,ce(function(e,t){for(var n,r=a([],e.length,i),o=r.length;o--;)e[n=r[o]]&&(e[n]=!(t[n]=e[n]))})})}function me(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in p=se.support={},a=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!K.test(t||n&&n.nodeName||\"HTML\")},C=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:y;return r!=T&&9===r.nodeType&&r.documentElement&&(s=(T=r).documentElement,k=!a(T),y!=T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener(\"unload\",o,!1):n.attachEvent&&n.attachEvent(\"onunload\",o)),p.scope=le(function(e){return s.appendChild(e).appendChild(T.createElement(\"div\")),void 0!==e.querySelectorAll&&!e.querySelectorAll(\":scope fieldset div\").length}),p.attributes=le(function(e){return e.className=\"i\",!e.getAttribute(\"className\")}),p.getElementsByTagName=le(function(e){return e.appendChild(T.createComment(\"\")),!e.getElementsByTagName(\"*\").length}),p.getElementsByClassName=ee.test(T.getElementsByClassName),p.getById=le(function(e){return s.appendChild(e).id=E,!T.getElementsByName||!T.getElementsByName(E).length}),p.getById?(w.filter.ID=function(e){var t=e.replace(re,f);return function(e){return e.getAttribute(\"id\")===t}},w.find.ID=function(e,t){if(void 0!==t.getElementById&&k){var n=t.getElementById(e);return n?[n]:[]}}):(w.filter.ID=function(e){var n=e.replace(re,f);return function(e){var t=void 0!==e.getAttributeNode&&e.getAttributeNode(\"id\");return t&&t.value===n}},w.find.ID=function(e,t){if(void 0!==t.getElementById&&k){var n,r,o,i=t.getElementById(e);if(i){if((n=i.getAttributeNode(\"id\"))&&n.value===e)return[i];for(o=t.getElementsByName(e),r=0;i=o[r++];)if((n=i.getAttributeNode(\"id\"))&&n.value===e)return[i]}return[]}}),w.find.TAG=p.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if(\"*\"!==e)return i;for(;n=i[o++];)1===n.nodeType&&r.push(n);return r},w.find.CLASS=p.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&k)return t.getElementsByClassName(e)},l=[],g=[],(p.qsa=ee.test(T.querySelectorAll))&&(le(function(e){var t;s.appendChild(e).innerHTML=\"\",e.querySelectorAll(\"[msallowcapture^='']\").length&&g.push(\"[*^$]=\"+B+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\"[selected]\").length||g.push(\"\\\\[\"+B+\"*(?:value|\"+I+\")\"),e.querySelectorAll(\"[id~=\"+E+\"-]\").length||g.push(\"~=\"),(t=T.createElement(\"input\")).setAttribute(\"name\",\"\"),e.appendChild(t),e.querySelectorAll(\"[name='']\").length||g.push(\"\\\\[\"+B+\"*name\"+B+\"*=\"+B+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\":checked\").length||g.push(\":checked\"),e.querySelectorAll(\"a#\"+E+\"+*\").length||g.push(\".#.+[+~]\"),e.querySelectorAll(\"\\\\\\f\"),g.push(\"[\\\\r\\\\n\\\\f]\")}),le(function(e){e.innerHTML=\"\";var t=T.createElement(\"input\");t.setAttribute(\"type\",\"hidden\"),e.appendChild(t).setAttribute(\"name\",\"D\"),e.querySelectorAll(\"[name=d]\").length&&g.push(\"name\"+B+\"*[*^$|!~]?=\"),2!==e.querySelectorAll(\":enabled\").length&&g.push(\":enabled\",\":disabled\"),s.appendChild(e).disabled=!0,2!==e.querySelectorAll(\":disabled\").length&&g.push(\":enabled\",\":disabled\"),e.querySelectorAll(\"*,:x\"),g.push(\",.*:\")})),(p.matchesSelector=ee.test(m=s.matches||s.webkitMatchesSelector||s.mozMatchesSelector||s.oMatchesSelector||s.msMatchesSelector))&&le(function(e){p.disconnectedMatch=m.call(e,\"*\"),m.call(e,\"[s!='']:x\"),l.push(\"!=\",$)}),g=g.length&&new RegExp(g.join(\"|\")),l=l.length&&new RegExp(l.join(\"|\")),t=ee.test(s.compareDocumentPosition),b=t||ee.test(s.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},O=t?function(e,t){if(e===t)return c=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==y&&b(y,e)?-1:t==T||t.ownerDocument==y&&b(y,t)?1:u?R(u,e)-R(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return c=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],s=[t];if(!o||!i)return e==T?-1:t==T?1:o?-1:i?1:u?R(u,e)-R(u,t):0;if(o===i)return de(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?de(a[r],s[r]):a[r]==y?-1:s[r]==y?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),p.matchesSelector&&k&&!D[t+\" \"]&&(!l||!l.test(t))&&(!g||!g.test(t)))try{var n=m.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){D(t,!0)}return 0\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(re,f),e[3]=(e[3]||e[4]||e[5]||\"\").replace(re,f),\"~=\"===e[2]&&(e[3]=\" \"+e[3]+\" \"),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),\"nth\"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*(\"even\"===e[3]||\"odd\"===e[3])),e[5]=+(e[7]+e[8]||\"odd\"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||\"\":n&&G.test(n)&&(t=h(n,!0))&&(t=n.indexOf(\")\",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(re,f).toLowerCase();return\"*\"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+\" \"];return t||(t=new RegExp(\"(^|\"+B+\")\"+e+\"(\"+B+\"|$)\"))&&N(e,function(e){return t.test(\"string\"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute(\"class\")||\"\")})},ATTR:function(n,r,o){return function(e){var t=se.attr(e,n);return null==t?\"!=\"===r:!r||(t+=\"\",\"=\"===r?t===o:\"!=\"===r?t!==o:\"^=\"===r?o&&0===t.indexOf(o):\"*=\"===r?o&&-1\",\"#\"===e.firstChild.getAttribute(\"href\")})||fe(\"type|href|height|width\",function(e,t,n){if(!n)return e.getAttribute(t,\"type\"===t.toLowerCase()?1:2)}),p.attributes&&le(function(e){return e.innerHTML=\"\",e.firstChild.setAttribute(\"value\",\"\"),\"\"===e.firstChild.getAttribute(\"value\")})||fe(\"value\",function(e,t,n){if(!n&&\"input\"===e.nodeName.toLowerCase())return e.defaultValue}),le(function(e){return null==e.getAttribute(\"disabled\")})||fe(I,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(T);E.find=p,E.expr=p.selectors,E.expr[\":\"]=E.expr.pseudos,E.uniqueSort=E.unique=p.uniqueSort,E.text=p.getText,E.isXMLDoc=p.isXML,E.contains=p.contains,E.escapeSelector=p.escape;function h(e,t,n){for(var r=[],o=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(o&&E(e).is(n))break;r.push(e)}return r}function C(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}var S=E.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var _=/^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;function A(e,n,r){return y(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):\"string\"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\\w-]+))$/;(E.fn.init=function(e,t,n){var r,o;if(!e)return this;if(n=n||D,\"string\"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):y(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this);if(!(r=\"<\"===e[0]&&\">\"===e[e.length-1]&&3<=e.length?[null,e,null]:O.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:k,!0)),_.test(r[1])&&E.isPlainObject(t))for(r in t)y(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(o=k.getElementById(r[2]))&&(this[0]=o,this.length=1),this}).prototype=E.fn,D=E(k);var j=/^(?:parents|prev(?:Until|All))/,M={children:!0,contents:!0,next:!0,prev:!0};function q(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\\x20\\t\\r\\n\\f]*)/i,he=/^$|^module$|\\/(?:java|ecma)script/i;le=k.createDocumentFragment().appendChild(k.createElement(\"div\")),(fe=k.createElement(\"input\")).setAttribute(\"type\",\"radio\"),fe.setAttribute(\"checked\",\"checked\"),fe.setAttribute(\"name\",\"t\"),le.appendChild(fe),b.checkClone=le.cloneNode(!0).cloneNode(!0).lastChild.checked,le.innerHTML=\"\",b.noCloneChecked=!!le.cloneNode(!0).lastChild.defaultValue,le.innerHTML=\"\",b.option=!!le.lastChild;var ve={thead:[1,\"\",\"
\"],col:[2,\"\",\"
\"],tr:[2,\"\",\"
\"],td:[3,\"\",\"
\"],_default:[0,\"\",\"\"]};function ge(e,t){var n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||\"*\"):void 0!==e.querySelectorAll?e.querySelectorAll(t||\"*\"):[];return void 0===t||t&&N(e,t)?E.merge([e],n):n}function me(e,t){for(var n=0,r=e.length;n\",\"\"]);var be=/<|&#?\\w+;/;function ye(e,t,n,r,o){for(var i,a,s,u,c,l,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\\s*$/g;function Oe(e,t){return N(e,\"table\")&&N(11!==t.nodeType?t:t.firstChild,\"tr\")&&E(e).children(\"tbody\")[0]||e}function je(e){return e.type=(null!==e.getAttribute(\"type\"))+\"/\"+e.type,e}function Me(e){return\"true/\"===(e.type||\"\").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute(\"type\"),e}function qe(e,t){var n,r,o,i,a,s;if(1===t.nodeType){if(X.hasData(e)&&(s=X.get(e).events))for(o in X.remove(t,\"handle events\"),s)for(n=0,r=s[o].length;n\").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on(\"load error\",o=function(e){r.remove(),o=null,e&&t(\"error\"===e.type?404:200,e.type)}),k.head.appendChild(r[0])},abort:function(){o&&o()}}});var tn,nn=[],rn=/(=)\\?(?=&|$)|\\?\\?/;E.ajaxSetup({jsonp:\"callback\",jsonpCallback:function(){var e=nn.pop()||E.expando+\"_\"+Mt.guid++;return this[e]=!0,e}}),E.ajaxPrefilter(\"json jsonp\",function(e,t,n){var r,o,i,a=!1!==e.jsonp&&(rn.test(e.url)?\"url\":\"string\"==typeof e.data&&0===(e.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&rn.test(e.data)&&\"data\");if(a||\"jsonp\"===e.dataTypes[0])return r=e.jsonpCallback=y(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(rn,\"$1\"+r):!1!==e.jsonp&&(e.url+=(qt.test(e.url)?\"&\":\"?\")+e.jsonp+\"=\"+r),e.converters[\"script json\"]=function(){return i||E.error(r+\" was not called\"),i[0]},e.dataTypes[0]=\"json\",o=T[r],T[r]=function(){i=arguments},n.always(function(){void 0===o?E(T).removeProp(r):T[r]=o,e[r]&&(e.jsonpCallback=t.jsonpCallback,nn.push(r)),i&&y(o)&&o(i[0]),i=o=void 0}),\"script\"}),b.createHTMLDocument=((tn=k.implementation.createHTMLDocument(\"\").body).innerHTML=\"
\",2===tn.childNodes.length),E.parseHTML=function(e,t,n){return\"string\"!=typeof e?[]:(\"boolean\"==typeof t&&(n=t,t=!1),t||(b.createHTMLDocument?((r=(t=k.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=k.location.href,t.head.appendChild(r)):t=k),i=!n&&[],(o=_.exec(e))?[t.createElement(o[1])]:(o=ye([e],t,i),i&&i.length&&E(i).remove(),E.merge([],o.childNodes)));var r,o,i},E.fn.load=function(e,t,n){var r,o,i,a=this,s=e.indexOf(\" \");return-1\").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,i||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,o,i,a,s,u,c=E.css(e,\"position\"),l=E(e),f={};\"static\"===c&&(e.style.position=\"relative\"),s=l.offset(),i=E.css(e,\"top\"),u=E.css(e,\"left\"),o=(\"absolute\"===c||\"fixed\"===c)&&-1<(i+u).indexOf(\"auto\")?(a=(r=l.position()).top,r.left):(a=parseFloat(i)||0,parseFloat(u)||0),y(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+o),\"using\"in t?t.using.call(e,f):(\"number\"==typeof f.top&&(f.top+=\"px\"),\"number\"==typeof f.left&&(f.left+=\"px\"),l.css(f))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],o={top:0,left:0};if(\"fixed\"===E.css(r,\"position\"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&\"static\"===E.css(e,\"position\");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((o=E(e).offset()).top+=E.css(e,\"borderTopWidth\",!0),o.left+=E.css(e,\"borderLeftWidth\",!0))}return{top:t.top-o.top-E.css(r,\"marginTop\",!0),left:t.left-o.left-E.css(r,\"marginLeft\",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&\"static\"===E.css(e,\"position\");)e=e.offsetParent;return e||re})}}),E.each({scrollLeft:\"pageXOffset\",scrollTop:\"pageYOffset\"},function(t,o){var i=\"pageYOffset\"===o;E.fn[t]=function(e){return $(this,function(e,t,n){var r;if(v(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[o]:e[t];r?r.scrollTo(i?r.pageXOffset:n,i?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each([\"top\",\"left\"],function(e,n){E.cssHooks[n]=Qe(b.pixelPosition,function(e,t){if(t)return t=Ke(e,n),Je.test(t)?E(e).position()[n]+\"px\":t})}),E.each({Height:\"height\",Width:\"width\"},function(a,s){E.each({padding:\"inner\"+a,content:s,\"\":\"outer\"+a},function(r,i){E.fn[i]=function(e,t){var n=arguments.length&&(r||\"boolean\"!=typeof e),o=r||(!0===e||!0===t?\"margin\":\"border\");return $(this,function(e,t,n){var r;return v(e)?0===i.indexOf(\"outer\")?e[\"inner\"+a]:e.document.documentElement[\"client\"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body[\"scroll\"+a],r[\"scroll\"+a],e.body[\"offset\"+a],r[\"offset\"+a],r[\"client\"+a])):void 0===n?E.css(e,t,o):E.style(e,t,n,o)},s,n?e:void 0,n)}})}),E.each([\"ajaxStart\",\"ajaxStop\",\"ajaxComplete\",\"ajaxError\",\"ajaxSuccess\",\"ajaxSend\"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,n){E.fn[n]=function(e,t){return 0e.length)&&e.substring(0,t.length)===t},vd:function(e,t){if(e===t)return!0;if(11===e.nodeType)return!1;if(t.contains)return t.contains(1!==e.nodeType?e.parentNode:e);if(t.compareDocumentPosition)return 16==(16&t.compareDocumentPosition(e));for(;e&&e!=t;)e=e.parentNode;return!!e},Sb:function(e){return _.a.vd(e,e.ownerDocument.documentElement)},kd:function(e){return!!_.a.Lb(e,_.a.Sb)},R:function(e){return e&&e.tagName&&e.tagName.toLowerCase()},Ac:function(e){return _.onError?function(){try{return e.apply(this,arguments)}catch(e){throw _.onError&&_.onError(e),e}}:e},setTimeout:(c=function(e,t){return setTimeout(_.a.Ac(e),t)},h.toString=function(){return c.toString()},h),Gc:function(e){setTimeout(function(){throw _.onError&&_.onError(e),e},0)},B:function(t,e,n){var r=_.a.Ac(n);if(n=l[e],_.options.useOnlyNativeEvents||n||!qe)if(n||\"function\"!=typeof t.addEventListener){if(void 0===t.attachEvent)throw Error(\"Browser doesn't support addEventListener or attachEvent\");var o=function(e){r.call(t,e)},i=\"on\"+e;t.attachEvent(i,o),_.a.K.za(t,function(){t.detachEvent(i,o)})}else t.addEventListener(e,r,!1);else u=u||(\"function\"==typeof qe(t).on?\"on\":\"bind\"),qe(t)[u](e,r)},Fb:function(e,t){if(!e||!e.nodeType)throw Error(\"element must be a DOM node when calling triggerEvent\");var n=!(\"input\"!==_.a.R(e)||!e.type||\"click\"!=t.toLowerCase())&&(\"checkbox\"==(n=e.type)||\"radio\"==n);if(_.options.useOnlyNativeEvents||!qe||n)if(\"function\"==typeof je.createEvent){if(\"function\"!=typeof e.dispatchEvent)throw Error(\"The supplied element doesn't support dispatchEvent\");(n=je.createEvent(s[t]||\"HTMLEvents\")).initEvent(t,!0,!0,Oe,0,0,0,0,0,!1,!1,!1,!1,0,e),e.dispatchEvent(n)}else if(n&&e.click)e.click();else{if(void 0===e.fireEvent)throw Error(\"Browser doesn't support triggering events\");e.fireEvent(\"on\"+t)}else qe(e).trigger(t)},f:function(e){return _.O(e)?e():e},bc:function(e){return _.O(e)?e.v():e},Eb:function(t,e,n){var r;e&&(\"object\"===_typeof(t.classList)?(r=t.classList[n?\"add\":\"remove\"],_.a.D(e.match(p),function(e){r.call(t.classList,e)})):\"string\"==typeof t.className.baseVal?o(t.className,\"baseVal\",e,n):o(t,\"className\",e,n))},Bb:function(e,t){var n=_.a.f(t);null!==n&&n!==De||(n=\"\");var r=_.h.firstChild(e);!r||3!=r.nodeType||_.h.nextSibling(r)?_.h.va(e,[e.ownerDocument.createTextNode(n)]):r.data=n,_.a.Ad(e)},Yc:function(e,t){if(e.name=t,d<=7)try{var n=e.name.replace(/[&<>'\"]/g,function(e){return\"&#\"+e.charCodeAt(0)+\";\"});e.mergeAttributes(je.createElement(\"\"),!1)}catch(e){}},Ad:function(e){9<=d&&((e=1==e.nodeType?e:e.parentNode).style&&(e.style.zoom=e.style.zoom))},wd:function(e){var t;d&&(t=e.style.width,e.style.width=0,e.style.width=t)},Pd:function(e,t){e=_.a.f(e),t=_.a.f(t);for(var n=[],r=e;r<=t;r++)n.push(r);return n},la:function(e){for(var t=[],n=0,r=e.length;n\",\"\"],tbody:t,tfoot:t,tr:[2,\"\",\"
\"],td:l=[3,\"\",\"
\"],th:l,option:f=[1,\"\"],optgroup:f},p=_.a.W<=8,_.a.ua=function(e,t){var n;if(qe){if(qe.parseHTML)n=qe.parseHTML(e,t)||[];else if((n=qe.clean([e],t))&&n[0]){for(var r=n[0];r.parentNode&&11!==r.parentNode.nodeType;)r=r.parentNode;r.parentNode&&r.parentNode.removeChild(r)}}else{(n=t)||(n=je);var r=n.parentWindow||n.defaultView||Oe,o=_.a.Db(e).toLowerCase(),i=n.createElement(\"div\"),a=(o=o.match(/^(?:\\x3c!--.*?--\\x3e\\s*?)*?<([a-z]+)[\\s>]/))&&d[o[1]]||u,o=a[0];for(a=\"ignored
\"+a[1]+e+a[2]+\"
\",\"function\"==typeof r.innerShiv?i.appendChild(r.innerShiv(a)):(p&&n.body.appendChild(i),i.innerHTML=a,p&&i.parentNode.removeChild(i));o--;)i=i.lastChild;n=_.a.la(i.lastChild.childNodes)}return n},_.a.Md=function(e,t){var n=_.a.ua(e,t);return n.length&&n[0].parentElement||_.a.Yb(n)},_.a.fc=function(e,t){if(_.a.Tb(e),null!==(t=_.a.f(t))&&t!==De)if(\"string\"!=typeof t&&(t=t.toString()),qe)qe(e).html(t);else for(var n=_.a.ua(t,e.ownerDocument),r=0;r]*))?)*\\s+)data-bind\\s*=\\s*([\"'])([\\s\\S]*?)\\3/gi,Te=/\\x3c!--\\s*ko\\b\\s*([\\s\\S]*?)\\s*--\\x3e/g,{xd:function(e,t,n){t.isTemplateRewritten(e,n)||t.rewriteTemplate(e,function(e){return _.kc.Ld(e,t)},n)},Ld:function(e,i){return e.replace(Ce,function(e,t,n,r,o){return Ne(o,t,n,i)}).replace(Te,function(e,t){return Ne(t,\"\\x3c!-- ko --\\x3e\",\"#comment\",i)})},md:function(r,o){return _.aa.Xb(function(e,t){var n=e.nextSibling;n&&n.nodeName.toLowerCase()===o&&_.ib(n,r,t)})}}),_.b(\"__tr_ambtns\",_.kc.md),function(){_.C={},_.C.F=function(e){var t;(this.F=e)&&(t=_.a.R(e),this.ab=\"script\"===t?1:\"textarea\"===t?2:\"template\"==t&&e.content&&11===e.content.nodeType?3:4)},_.C.F.prototype.text=function(){var e=1===this.ab?\"text\":2===this.ab?\"value\":\"innerHTML\";if(0==arguments.length)return this.F[e];var t=arguments[0];\"innerHTML\"==e?_.a.fc(this.F,t):this.F[e]=t};var t=_.a.g.Z()+\"_\";_.C.F.prototype.data=function(e){if(1===arguments.length)return _.a.g.get(this.F,t+e);_.a.g.set(this.F,t+e,arguments[1])};var o=_.a.g.Z();_.C.F.prototype.nodes=function(){var e=this.F;if(0==arguments.length){var t,n=_.a.g.get(e,o)||{},r=n.lb||(3===this.ab?e.content:4===this.ab?e:De);return r&&!n.jd||(t=this.text())&&t!==n.bb&&(r=_.a.Md(t,e.ownerDocument),_.a.g.set(e,o,{lb:r,bb:t,jd:!0})),r}n=arguments[0],this.ab!==De&&this.text(\"\"),_.a.g.set(e,o,{lb:n})},_.C.ia=function(e){this.F=e},_.C.ia.prototype=new _.C.F,_.C.ia.prototype.constructor=_.C.ia,_.C.ia.prototype.text=function(){if(0==arguments.length){var e=_.a.g.get(this.F,o)||{};return e.bb===De&&e.lb&&(e.bb=e.lb.innerHTML),e.bb}_.a.g.set(this.F,o,{bb:arguments[0]})},_.b(\"templateSources\",_.C),_.b(\"templateSources.domElement\",_.C.F),_.b(\"templateSources.anonymousTemplate\",_.C.ia)}(),function(){function r(e,t,n){var r;for(t=_.h.nextSibling(t);e&&(r=e)!==t;)n(r,e=_.h.nextSibling(r))}function d(e,t){if(e.length){var o=e[0],i=e[e.length-1],n=o.parentNode,a=_.ga.instance,s=a.preprocessNode;if(s){if(r(o,i,function(e,t){var n=e.previousSibling,r=s.call(a,e);r&&(e===o&&(o=r[0]||t),e===i&&(i=r[r.length-1]||n))}),e.length=0,!o)return;o===i?e.push(o):(e.push(o,i),_.a.Ua(e,n))}r(o,i,function(e){1!==e.nodeType&&8!==e.nodeType||_.vc(t,e)}),r(o,i,function(e){1!==e.nodeType&&8!==e.nodeType||_.aa.cd(e,[t])}),_.a.Ua(e,n)}}function u(e){return e.nodeType?e:0\"+t+\"<\\/script>\")},0 OpenEVSE

OpenEVSE

WiFi

WiFi Setup

Mode:

Connect to network:

Select Network RSSI dBm
Scanning...

Passkey:

Connecting to a local WiFi network is not essential. OpenEVSE can be configured and controlled while in standalone WiFi AP (Access Point) mode:

Note: remote logging features e.g Emoncms will not work while in AP standalone mode

Connecting to ...

Please connect this device to and navigate to the IP address displayed on your OpenEVSE display.

Alternatively you can use http://openevse.local/ or http://openevse/


Powered by OpenEVSE and OpenEnergyMonitor
Version: V
\n"; + " OpenEVSE

OpenEVSE

WiFi

WiFi Setup

Mode:

Connect to network:

Scanning...

SSID:

Passkey:

Connecting to a local WiFi network is not essential. OpenEVSE can be configured and controlled while in standalone WiFi AP (Access Point) mode:

Note: remote logging features e.g Emoncms will not work while in AP standalone mode

Connecting to ...

Please connect this device to and navigate to the IP address displayed on your OpenEVSE display.

Alternatively you can use or


Powered by OpenEVSE and OpenEnergyMonitor
Version: V
\n"; diff --git a/src/web_static/web_server.wifi_portal.js.h b/src/web_static/web_server.wifi_portal.js.h index baab23d..558ecc3 100644 --- a/src/web_static/web_server.wifi_portal.js.h +++ b/src/web_static/web_server.wifi_portal.js.h @@ -1,3 +1,3 @@ static const char CONTENT_WIFI_PORTAL_JS[] PROGMEM = - "\"use strict\";function WiFiPortalViewModel(t,n){var e=this;e.baseHost=ko.observable(\"\"!==t?t:\"openevse.local\"),e.basePort=ko.observable(n),e.baseEndpoint=ko.pureComputed(function(){var t=\"//\"+e.baseHost();return 80!==e.basePort()&&(t+=\":\"+e.basePort()),t}),e.config=new ConfigViewModel(e.baseEndpoint),e.status=new StatusViewModel(e.baseEndpoint),e.scan=new WiFiScanViewModel(e.baseEndpoint),e.wifi=new WiFiConfigViewModel(e.baseEndpoint,e.config,e.status,e.scan),e.initialised=ko.observable(!1),e.updating=ko.observable(!1);var i=null;e.start=function(){e.updating(!0),e.config.update(function(){e.status.update(function(){e.initialised(!0),i=setTimeout(e.update,5e3),e.updating(!1)})})},e.update=function(){e.updating()||(e.updating(!0),null!==i&&(clearTimeout(i),i=null),e.status.update(function(){i=setTimeout(e.update,5e3),e.updating(!1)}))}}function scaleString(t,n,e){return(parseInt(t)/n).toFixed(e)}!function(){var n=window.location.hostname,e=window.location.port;$(function(){var t=new WiFiPortalViewModel(n,e);ko.applyBindings(t),t.start()})}();\n" + "\"use strict\";function WiFiPortalViewModel(i,n){var e=this;e.baseHost=ko.observable(\"\"!==i?i:\"openevse.local\"),e.basePort=ko.observable(n),e.baseEndpoint=ko.pureComputed(function(){var i=\"//\"+e.baseHost();return 80!==e.basePort()&&(i+=\":\"+e.basePort()),i}),e.config=new ConfigViewModel(e.baseEndpoint),e.status=new StatusViewModel(e.baseEndpoint),e.scan=new WiFiScanViewModel(e.baseEndpoint),e.wifi=new WiFiConfigViewModel(e.baseEndpoint,e.config,e.status,e.scan),e.initialised=ko.observable(!1),e.updating=ko.observable(!1),e.wifi.selectedNet.subscribe(function(i){!1!==i&&e.config.ssid(i.ssid())}),e.config.ssid.subscribe(function(i){e.wifi.setSsid(i)}),e.wifiPassword=new PasswordViewModel(e.config.pass);var t=null;e.start=function(){e.updating(!0),e.config.update(function(){e.status.update(function(){e.initialised(!0),t=setTimeout(e.update,5e3),e.updating(!1)})})},e.update=function(){e.updating()||(e.updating(!0),null!==t&&(clearTimeout(t),t=null),e.status.update(function(){t=setTimeout(e.update,5e3),e.updating(!1)}))}}function scaleString(i,n,e){return(parseInt(i)/n).toFixed(e)}!function(){var n=window.location.hostname,e=window.location.port;$(function(){var i=new WiFiPortalViewModel(n,e);ko.applyBindings(i),i.start()})}();\n" "//# sourceMappingURL=wifi_portal.js.map\n"; diff --git a/src/web_static/web_server.wifi_signal_1.svg.h b/src/web_static/web_server.wifi_signal_1.svg.h new file mode 100644 index 0000000..dd22319 --- /dev/null +++ b/src/web_static/web_server.wifi_signal_1.svg.h @@ -0,0 +1,68 @@ +static const char CONTENT_WIFI_SIGNAL_1_SVG[] PROGMEM = + "\n" + "\n" + " \n" + " \n" + " \n" + " image/svg+xml\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; diff --git a/src/web_static/web_server.wifi_signal_2.svg.h b/src/web_static/web_server.wifi_signal_2.svg.h new file mode 100644 index 0000000..a20670e --- /dev/null +++ b/src/web_static/web_server.wifi_signal_2.svg.h @@ -0,0 +1,67 @@ +static const char CONTENT_WIFI_SIGNAL_2_SVG[] PROGMEM = + "\n" + "\n" + " \n" + " \n" + " \n" + " image/svg+xml\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; diff --git a/src/web_static/web_server.wifi_signal_3.svg.h b/src/web_static/web_server.wifi_signal_3.svg.h new file mode 100644 index 0000000..0970954 --- /dev/null +++ b/src/web_static/web_server.wifi_signal_3.svg.h @@ -0,0 +1,66 @@ +static const char CONTENT_WIFI_SIGNAL_3_SVG[] PROGMEM = + "\n" + "\n" + " \n" + " \n" + " \n" + " image/svg+xml\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; diff --git a/src/web_static/web_server.wifi_signal_4.svg.h b/src/web_static/web_server.wifi_signal_4.svg.h new file mode 100644 index 0000000..0132e92 --- /dev/null +++ b/src/web_static/web_server.wifi_signal_4.svg.h @@ -0,0 +1,65 @@ +static const char CONTENT_WIFI_SIGNAL_4_SVG[] PROGMEM = + "\n" + "\n" + " \n" + " \n" + " \n" + " image/svg+xml\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; diff --git a/src/web_static/web_server.wifi_signal_5.svg.h b/src/web_static/web_server.wifi_signal_5.svg.h new file mode 100644 index 0000000..8405c9a --- /dev/null +++ b/src/web_static/web_server.wifi_signal_5.svg.h @@ -0,0 +1,55 @@ +static const char CONTENT_WIFI_SIGNAL_5_SVG[] PROGMEM = + "\n" + "\n" + " \n" + " \n" + " \n" + " image/svg+xml\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; diff --git a/src/web_static/web_server_static_files.h b/src/web_static/web_server_static_files.h index ade7693..bba8449 100644 --- a/src/web_static/web_server_static_files.h +++ b/src/web_static/web_server_static_files.h @@ -9,6 +9,11 @@ #include "web_server.style.css.h" #include "web_server.wifi_portal.html.h" #include "web_server.wifi_portal.js.h" +#include "web_server.wifi_signal_1.svg.h" +#include "web_server.wifi_signal_2.svg.h" +#include "web_server.wifi_signal_3.svg.h" +#include "web_server.wifi_signal_4.svg.h" +#include "web_server.wifi_signal_5.svg.h" StaticFile staticFiles[] = { { "/assets.js", CONTENT_ASSETS_JS, sizeof(CONTENT_ASSETS_JS) - 1, _CONTENT_TYPE_JS }, { "/emoncms.jpg", CONTENT_EMONCMS_JPG, sizeof(CONTENT_EMONCMS_JPG) - 1, _CONTENT_TYPE_JPEG }, @@ -21,4 +26,9 @@ StaticFile staticFiles[] = { { "/style.css", CONTENT_STYLE_CSS, sizeof(CONTENT_STYLE_CSS) - 1, _CONTENT_TYPE_CSS }, { "/wifi_portal.html", CONTENT_WIFI_PORTAL_HTML, sizeof(CONTENT_WIFI_PORTAL_HTML) - 1, _CONTENT_TYPE_HTML }, { "/wifi_portal.js", CONTENT_WIFI_PORTAL_JS, sizeof(CONTENT_WIFI_PORTAL_JS) - 1, _CONTENT_TYPE_JS }, + { "/wifi_signal_1.svg", CONTENT_WIFI_SIGNAL_1_SVG, sizeof(CONTENT_WIFI_SIGNAL_1_SVG) - 1, _CONTENT_TYPE_SVG }, + { "/wifi_signal_2.svg", CONTENT_WIFI_SIGNAL_2_SVG, sizeof(CONTENT_WIFI_SIGNAL_2_SVG) - 1, _CONTENT_TYPE_SVG }, + { "/wifi_signal_3.svg", CONTENT_WIFI_SIGNAL_3_SVG, sizeof(CONTENT_WIFI_SIGNAL_3_SVG) - 1, _CONTENT_TYPE_SVG }, + { "/wifi_signal_4.svg", CONTENT_WIFI_SIGNAL_4_SVG, sizeof(CONTENT_WIFI_SIGNAL_4_SVG) - 1, _CONTENT_TYPE_SVG }, + { "/wifi_signal_5.svg", CONTENT_WIFI_SIGNAL_5_SVG, sizeof(CONTENT_WIFI_SIGNAL_5_SVG) - 1, _CONTENT_TYPE_SVG }, }; diff --git a/src/wifi.cpp b/src/wifi.cpp index 302177c..4de1a71 100644 --- a/src/wifi.cpp +++ b/src/wifi.cpp @@ -1,6 +1,6 @@ #include "emonesp.h" #include "wifi.h" -#include "config.h" +#include "app_config.h" #include "lcd.h" #include // Connect to Wifi From ae695db9325e2e9e53a32e3d2713c5b43557c16c Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Thu, 18 Jun 2020 22:19:09 +0100 Subject: [PATCH 04/18] Only send the disconnected message when we where connected --- src/emoncms.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/emoncms.cpp b/src/emoncms.cpp index 401353d..a12b7e1 100644 --- a/src/emoncms.cpp +++ b/src/emoncms.cpp @@ -87,7 +87,9 @@ void emoncms_publish(JsonDocument &data) emoncms_result(false, result); } } else { - emoncms_result(false, String("Disabled")); + if(emoncms_connected) { + emoncms_result(false, String("Disabled")); + } } Profile_End(emoncms_publish, 10); From b56db64d4e10d453fc50a15104556a2aaa4ad860 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Thu, 18 Jun 2020 22:37:55 +0100 Subject: [PATCH 05/18] Fix off by 1 error --- src/lcd.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lcd.cpp b/src/lcd.cpp index 894906b..9383313 100644 --- a/src/lcd.cpp +++ b/src/lcd.cpp @@ -13,7 +13,7 @@ typedef struct Message_s Message; struct Message_s { Message *next; - char msg[LCD_MAX_LEN]; + char msg[LCD_MAX_LEN + 1]; int x; int y; int time; From 171a397b4c8d437e65f50b29818904bf6b153d1e Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Thu, 18 Jun 2020 22:38:17 +0100 Subject: [PATCH 06/18] Improved memory display --- src/src.ino | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/src.ino b/src/src.ino index e266d78..42c6dd9 100644 --- a/src/src.ino +++ b/src/src.ino @@ -51,6 +51,9 @@ unsigned long Timer3; // Timer for events once every 2 seconds boolean rapi_read = 0; //flag to indicate first read of RAPI status +static uint32_t start_mem = 0; +static uint32_t last_mem = 0; + static void hardware_setup(); // ------------------------------------------------------------------- @@ -86,6 +89,7 @@ void setup() input_setup(); + start_mem = last_mem = ESPAL.getFreeHeap(); } // end setup // ------------------------------------------------------------------- @@ -125,7 +129,12 @@ loop() { // Do these things once every 2s // ------------------------------------------------------------------- if ((millis() - Timer3) >= 2000) { - DEBUG.printf("Free: %d\n", ESPAL.getFreeHeap()); + uint32_t current = ESPAL.getFreeHeap(); + int32_t diff = (int32_t)(last_mem - current); + if(diff != 0) { + DEBUG.printf("Free memory %u - diff %d %d\n", current, diff, start_mem - current); + last_mem = current; + } update_rapi_values(); Timer3 = millis(); } From 6c7e3e9137839f60a0738978db86626e16678712 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Thu, 18 Jun 2020 22:38:39 +0100 Subject: [PATCH 07/18] Fixed saving the config settings --- src/web_server.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/src/web_server.cpp b/src/web_server.cpp index d12e162..11a68ee 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -122,9 +122,9 @@ bool isPositive(const String &str) { } bool isPositive(AsyncWebServerRequest *request, const char *param) { - char paramValue[8]; - int paramFound = request->hasArg(param); - return paramFound >= 0 && (0 == paramFound || isPositive(String(paramValue))); + bool paramFound = request->hasArg(param); + String arg = request->arg(param); + return paramFound && (0 == arg.length() || isPositive(arg)); } // ------------------------------------------------------------------- @@ -493,7 +493,7 @@ handleStatus(AsyncWebServerRequest *request) { // url: /config // ------------------------------------------------------------------- void -handleConfig(AsyncWebServerRequest *request) { +handleConfigGet(AsyncWebServerRequest *request) { AsyncResponseStream *response; if(false == requestPreProcess(request, response)) { return; @@ -524,6 +524,37 @@ handleConfig(AsyncWebServerRequest *request) { request->send(response); } +void +handleConfigPost(AsyncWebServerRequest *request) +{ + AsyncResponseStream *response; + if(false == requestPreProcess(request, response)) { + return; + } + + if(request->_tempObject) + { + String *body = (String *)request->_tempObject; + + if(config_deserialize(*body)) { + config_commit(); + response->setCode(200); + response->print("{\"msg\":\"done\"}"); + } else { + response->setCode(400); + response->print("{\"msg\":\"Could not parse JSON\"}"); + } + + delete body; + request->_tempObject = NULL; + } else { + response->setCode(400); + response->print("{\"msg\":\"No Body\"}"); + } + + request->send(response); +} + #ifdef ENABLE_LEGACY_API // ------------------------------------------------------------------- // Returns Updates JSON @@ -854,6 +885,20 @@ void handleNotFound(AsyncWebServerRequest *request) } } +void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) +{ + if(!index) { + DBUGF("BodyStart: %u", total); + request->_tempObject = new String(); + } + String *body = (String *)request->_tempObject; + DBUGF("%.*s", len, (const char*)data); + body->concat((const char*)data, len); + if(index + len == total) { + DBUGF("BodyEnd: %u", total); + } +} + void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { if(type == WS_EVT_CONNECT) { DBUGF("ws[%s][%u] connect", server->url(), client->id()); @@ -895,7 +940,8 @@ web_server_setup() { // Handle status updates server.on("/status", handleStatus); - server.on("/config", handleConfig); + server.on("/config", HTTP_GET, handleConfigGet); + server.on("/config", HTTP_POST, handleConfigPost, NULL, handleBody); #ifdef ENABLE_LEGACY_API server.on("/rapiupdate", handleUpdate); #endif @@ -921,6 +967,8 @@ web_server_setup() { server.on("/update", HTTP_POST, handleUpdatePost, handleUpdateUpload); server.onNotFound(handleNotFound); + server.onRequestBody(handleBody); + server.begin(); DEBUG.println("Server started"); From 0464721cebf56fdda18209624fce30a88ad8dbbd Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Tue, 23 Jun 2020 23:56:00 +0100 Subject: [PATCH 08/18] Latest GUI and assosiated spelling fix --- gui | 2 +- src/divert.cpp | 34 +++++++++++++-------------- src/web_static/web_server.home.html.h | 4 ++-- src/web_static/web_server.lib.js.h | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/gui b/gui index 6ebd1a5..010714a 160000 --- a/gui +++ b/gui @@ -1 +1 @@ -Subproject commit 6ebd1a5d58d09092937baa4fa58f4476fb1cfc2a +Subproject commit 010714af0a115db91fbb1ed9270e8641edff4bca diff --git a/src/divert.cpp b/src/divert.cpp index fbe23d8..be9dd76 100644 --- a/src/divert.cpp +++ b/src/divert.cpp @@ -39,8 +39,8 @@ int last_state = OPENEVSE_STATE_INVALID; uint32_t lastUpdate = 0; -double avalible_current = 0; -double smoothed_avalible_current = 0; +double available_current = 0; +double smoothed_available_current = 0; time_t min_charge_end = 0; @@ -76,8 +76,8 @@ void divertmode_update(byte newmode) case DIVERT_MODE_ECO: charge_rate = 0; - avalible_current = 0; - smoothed_avalible_current = 0; + available_current = 0; + smoothed_available_current = 0; min_charge_end = 0; // Read the current charge current, assume this is the max set by the user @@ -164,31 +164,31 @@ void divert_update_state() // If excess power double reserve = GRID_IE_RESERVE_POWER / voltage; DBUGVAR(reserve); - avalible_current = (-Igrid_ie - reserve); + available_current = (-Igrid_ie - reserve); } else { // no excess, so use the min charge - avalible_current = 0; + available_current = 0; } } else if (mqtt_solar!="") { // if grid feed is not available: charge rate = solar generation DBUGVAR(voltage); - avalible_current = (double)solar / voltage; + available_current = (double)solar / voltage; } - if(avalible_current < 0) { - avalible_current = 0; + if(available_current < 0) { + available_current = 0; } - DBUGVAR(avalible_current); + DBUGVAR(available_current); - double scale = avalible_current > smoothed_avalible_current ? divert_attack_smoothing_factor : divert_decay_smoothing_factor; - smoothed_avalible_current = (avalible_current * scale) + (smoothed_avalible_current * (1 - scale)); - DBUGVAR(smoothed_avalible_current); + double scale = available_current > smoothed_available_current ? divert_attack_smoothing_factor : divert_decay_smoothing_factor; + smoothed_available_current = (available_current * scale) + (smoothed_available_current * (1 - scale)); + DBUGVAR(smoothed_available_current); - charge_rate = (int)floor(avalible_current); + charge_rate = (int)floor(available_current); if(OPENEVSE_STATE_SLEEPING != state) { // If we are not sleeping, make sure we are the minimum current @@ -197,7 +197,7 @@ void divert_update_state() DBUGVAR(charge_rate); - if(smoothed_avalible_current >= min_charge_current) + if(smoothed_available_current >= min_charge_current) { // Cap the charge rate at the configured maximum charge_rate = min(charge_rate, static_cast(max_charge_current)); @@ -279,8 +279,8 @@ void divert_update_state() event["charge_rate"] = charge_rate; event["voltage"] = voltage; - event["avalible_current"] = avalible_current; - event["smoothed_avalible_current"] = smoothed_avalible_current; + event["available_current"] = available_current; + event["smoothed_available_current"] = smoothed_available_current; } // end ecomode event_send(event); diff --git a/src/web_static/web_server.home.html.h b/src/web_static/web_server.home.html.h index e18fabb..0e014c4 100644 --- a/src/web_static/web_server.home.html.h +++ b/src/web_static/web_server.home.html.h @@ -9,11 +9,11 @@ static const char CONTENT_HOME_HTML[] PROGMEM = " optionsText: function(item) {\n" " return item + '://'\n" " },\n" - " value: config.mqtt_protocol\">
e.g 'emonpi', 'test.mosquitto.org', '192.168.1.4'

Port*:

Reject self-signed certificates:

Warning!!

Certificate validation is disabled, although the connection to MQTT server will be encrypted the connection is still vunrable to man-in-the-middle attacks.

Username: blank - no authentication

Password: blank - no authentication

Base-topic*:

e.g 'openevse'

Voltage topic:
Voltage MQTT topic to improve power calculations

  Connected: 

  OhmConnect

Click Here to Join

OhmConnect monitors real-time conditions on the electricity grid. When dirty and unsustainable power plants turn on, our users receive a notification to save energy.

Ohm Hour:

Ohm key:

USA - California only

Ohm Key can be obtained by logging in to OhmConnect, enter Settings and locate the link in \"Open Source Projects\"
Example: https://login.ohmconnect.com/verify-ohm-hour/OpnEoVse
Key: OpnEoVse

Solar PV divert

MQTT not enabled.

Solar PV Divert requires an SolarPV-gen or Grid (+I/-E) feed to be delivered via MQTT.
Dynamically adjust charge rate based on solar PV generation or excess power (grid export).

  • If only solar PV feed available: charge rate is modulated based on solar PV generation.
  • If grid +I/-E (positive Import / negative Export) feed is available: charge rate will be modulated by available excess power.
  • If EVSE is sleeping: charging will begin when solar PV / excess power > min charge rate.
  • Charging will pause if the excess power drops below the nim charge rate for a period of time.
Note: It's assumed that EVSE power is included in the grid feed


Solar: Grid Import/Export: W - | Charge rate: A

Feed*:

Solar PV MQTT topic to modulate charge rate based on solar Grid (+I/-E) MQTT topic to modulate charge rate based on excess power

Divert smoothing attack:

The amount of the new feed value to add to the divert avalible power rolling average

Divert smoothing decay:

The amount of the new feed value to remove to the divert avalible power rolling average

Minimum charge time:

The minimum amount of time (seconds) to charge the car once enabled via the Solar PV divert. This can help minimise wear and tare on the EVSE.

EVSE Error

EVSE Error

OpenEVSE not responding or not connected

e.g 'emonpi', 'test.mosquitto.org', '192.168.1.4'

Port*:

Reject self-signed certificates:

Warning!!

Certificate validation is disabled, although the connection to MQTT server will be encrypted the connection is still vunrable to man-in-the-middle attacks.

Username: blank - no authentication

Password: blank - no authentication

Base-topic*:

e.g 'openevse'

Voltage topic:
Voltage MQTT topic to improve power calculations

  Connected: 

  OhmConnect

Click Here to Join

OhmConnect monitors real-time conditions on the electricity grid. When dirty and unsustainable power plants turn on, our users receive a notification to save energy.

Ohm Hour:

Ohm key:

USA - California only

Ohm Key can be obtained by logging in to OhmConnect, enter Settings and locate the link in \"Open Source Projects\"
Example: https://login.ohmconnect.com/verify-ohm-hour/OpnEoVse
Key: OpnEoVse

Solar PV divert

MQTT not enabled.

Solar PV Divert requires an SolarPV-gen or Grid (+I/-E) feed to be delivered via MQTT.
Dynamically adjust charge rate based on solar PV generation or excess power (grid export).

  • If only solar PV feed available: charge rate is modulated based on solar PV generation.
  • If grid +I/-E (positive Import / negative Export) feed is available: charge rate will be modulated by available excess power.
  • If EVSE is sleeping: charging will begin when solar PV / excess power > min charge rate.
  • Charging will pause if the excess power drops below the nim charge rate for a period of time.
Note: It's assumed that EVSE power is included in the grid feed


Solar: Grid Import/Export: W - | Charge rate: A

Feed*:

Solar PV MQTT topic to modulate charge rate based on solar Grid (+I/-E) MQTT topic to modulate charge rate based on excess power

Divert smoothing attack:

The amount of the new feed value to add to the divert available power rolling average

Divert smoothing decay:

The amount of the new feed value to remove to the divert available power rolling average

Minimum charge time:

The minimum amount of time (seconds) to charge the car once enabled via the Solar PV divert. This can help minimise wear and tare on the EVSE.

EVSE Error

EVSE Error

OpenEVSE not responding or not connected

Session Status

Current Energy Temp Elapsed

Solar: Grid Import/Export: W - | Vehicle not connected Waiting for solar Charging from solar | Charge rate: A
Voltage: V | Avalible Current: A | Smoothed Current: A

Charge Options

Normal (fast) Eco (PV divert)


Time limit:

Eco (PV divert)


Time limit:
\",e.querySelectorAll(\"[msallowcapture^='']\").length&&g.push(\"[*^$]=\"+B+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\"[selected]\").length||g.push(\"\\\\[\"+B+\"*(?:value|\"+I+\")\"),e.querySelectorAll(\"[id~=\"+E+\"-]\").length||g.push(\"~=\"),(t=T.createElement(\"input\")).setAttribute(\"name\",\"\"),e.appendChild(t),e.querySelectorAll(\"[name='']\").length||g.push(\"\\\\[\"+B+\"*name\"+B+\"*=\"+B+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\":checked\").length||g.push(\":checked\"),e.querySelectorAll(\"a#\"+E+\"+*\").length||g.push(\".#.+[+~]\"),e.querySelectorAll(\"\\\\\\f\"),g.push(\"[\\\\r\\\\n\\\\f]\")}),le(function(e){e.innerHTML=\"\";var t=T.createElement(\"input\");t.setAttribute(\"type\",\"hidden\"),e.appendChild(t).setAttribute(\"name\",\"D\"),e.querySelectorAll(\"[name=d]\").length&&g.push(\"name\"+B+\"*[*^$|!~]?=\"),2!==e.querySelectorAll(\":enabled\").length&&g.push(\":enabled\",\":disabled\"),s.appendChild(e).disabled=!0,2!==e.querySelectorAll(\":disabled\").length&&g.push(\":enabled\",\":disabled\"),e.querySelectorAll(\"*,:x\"),g.push(\",.*:\")})),(p.matchesSelector=ee.test(m=s.matches||s.webkitMatchesSelector||s.mozMatchesSelector||s.oMatchesSelector||s.msMatchesSelector))&&le(function(e){p.disconnectedMatch=m.call(e,\"*\"),m.call(e,\"[s!='']:x\"),l.push(\"!=\",$)}),g=g.length&&new RegExp(g.join(\"|\")),l=l.length&&new RegExp(l.join(\"|\")),t=ee.test(s.compareDocumentPosition),b=t||ee.test(s.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},O=t?function(e,t){if(e===t)return c=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==y&&b(y,e)?-1:t==T||t.ownerDocument==y&&b(y,t)?1:u?R(u,e)-R(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return c=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],s=[t];if(!o||!i)return e==T?-1:t==T?1:o?-1:i?1:u?R(u,e)-R(u,t):0;if(o===i)return de(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?de(a[r],s[r]):a[r]==y?-1:s[r]==y?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),p.matchesSelector&&k&&!D[t+\" \"]&&(!l||!l.test(t))&&(!g||!g.test(t)))try{var n=m.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){D(t,!0)}return 0\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(re,f),e[3]=(e[3]||e[4]||e[5]||\"\").replace(re,f),\"~=\"===e[2]&&(e[3]=\" \"+e[3]+\" \"),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),\"nth\"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*(\"even\"===e[3]||\"odd\"===e[3])),e[5]=+(e[7]+e[8]||\"odd\"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||\"\":n&&G.test(n)&&(t=h(n,!0))&&(t=n.indexOf(\")\",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(re,f).toLowerCase();return\"*\"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+\" \"];return t||(t=new RegExp(\"(^|\"+B+\")\"+e+\"(\"+B+\"|$)\"))&&N(e,function(e){return t.test(\"string\"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute(\"class\")||\"\")})},ATTR:function(n,r,o){return function(e){var t=se.attr(e,n);return null==t?\"!=\"===r:!r||(t+=\"\",\"=\"===r?t===o:\"!=\"===r?t!==o:\"^=\"===r?o&&0===t.indexOf(o):\"*=\"===r?o&&-1\",\"#\"===e.firstChild.getAttribute(\"href\")})||fe(\"type|href|height|width\",function(e,t,n){if(!n)return e.getAttribute(t,\"type\"===t.toLowerCase()?1:2)}),p.attributes&&le(function(e){return e.innerHTML=\"\",e.firstChild.setAttribute(\"value\",\"\"),\"\"===e.firstChild.getAttribute(\"value\")})||fe(\"value\",function(e,t,n){if(!n&&\"input\"===e.nodeName.toLowerCase())return e.defaultValue}),le(function(e){return null==e.getAttribute(\"disabled\")})||fe(I,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(T);E.find=p,E.expr=p.selectors,E.expr[\":\"]=E.expr.pseudos,E.uniqueSort=E.unique=p.uniqueSort,E.text=p.getText,E.isXMLDoc=p.isXML,E.contains=p.contains,E.escapeSelector=p.escape;function h(e,t,n){for(var r=[],o=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(o&&E(e).is(n))break;r.push(e)}return r}function C(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}var S=E.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var _=/^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;function A(e,n,r){return y(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):\"string\"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\\w-]+))$/;(E.fn.init=function(e,t,n){var r,o;if(!e)return this;if(n=n||D,\"string\"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):y(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this);if(!(r=\"<\"===e[0]&&\">\"===e[e.length-1]&&3<=e.length?[null,e,null]:O.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:k,!0)),_.test(r[1])&&E.isPlainObject(t))for(r in t)y(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(o=k.getElementById(r[2]))&&(this[0]=o,this.length=1),this}).prototype=E.fn,D=E(k);var j=/^(?:parents|prev(?:Until|All))/,M={children:!0,contents:!0,next:!0,prev:!0};function q(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\\x20\\t\\r\\n\\f]*)/i,he=/^$|^module$|\\/(?:java|ecma)script/i;le=k.createDocumentFragment().appendChild(k.createElement(\"div\")),(fe=k.createElement(\"input\")).setAttribute(\"type\",\"radio\"),fe.setAttribute(\"checked\",\"checked\"),fe.setAttribute(\"name\",\"t\"),le.appendChild(fe),b.checkClone=le.cloneNode(!0).cloneNode(!0).lastChild.checked,le.innerHTML=\"\",b.noCloneChecked=!!le.cloneNode(!0).lastChild.defaultValue,le.innerHTML=\"\",b.option=!!le.lastChild;var ve={thead:[1,\"\",\"
\"],col:[2,\"\",\"
\"],tr:[2,\"\",\"
\"],td:[3,\"\",\"
\"],_default:[0,\"\",\"\"]};function ge(e,t){var n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||\"*\"):void 0!==e.querySelectorAll?e.querySelectorAll(t||\"*\"):[];return void 0===t||t&&N(e,t)?E.merge([e],n):n}function me(e,t){for(var n=0,r=e.length;n\",\"\"]);var be=/<|&#?\\w+;/;function ye(e,t,n,r,o){for(var i,a,s,u,c,l,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\\s*$/g;function Oe(e,t){return N(e,\"table\")&&N(11!==t.nodeType?t:t.firstChild,\"tr\")&&E(e).children(\"tbody\")[0]||e}function je(e){return e.type=(null!==e.getAttribute(\"type\"))+\"/\"+e.type,e}function Me(e){return\"true/\"===(e.type||\"\").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute(\"type\"),e}function qe(e,t){var n,r,o,i,a,s;if(1===t.nodeType){if(X.hasData(e)&&(s=X.get(e).events))for(o in X.remove(t,\"handle events\"),s)for(n=0,r=s[o].length;n\").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on(\"load error\",o=function(e){r.remove(),o=null,e&&t(\"error\"===e.type?404:200,e.type)}),k.head.appendChild(r[0])},abort:function(){o&&o()}}});var tn,nn=[],rn=/(=)\\?(?=&|$)|\\?\\?/;E.ajaxSetup({jsonp:\"callback\",jsonpCallback:function(){var e=nn.pop()||E.expando+\"_\"+Mt.guid++;return this[e]=!0,e}}),E.ajaxPrefilter(\"json jsonp\",function(e,t,n){var r,o,i,a=!1!==e.jsonp&&(rn.test(e.url)?\"url\":\"string\"==typeof e.data&&0===(e.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&rn.test(e.data)&&\"data\");if(a||\"jsonp\"===e.dataTypes[0])return r=e.jsonpCallback=y(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(rn,\"$1\"+r):!1!==e.jsonp&&(e.url+=(qt.test(e.url)?\"&\":\"?\")+e.jsonp+\"=\"+r),e.converters[\"script json\"]=function(){return i||E.error(r+\" was not called\"),i[0]},e.dataTypes[0]=\"json\",o=T[r],T[r]=function(){i=arguments},n.always(function(){void 0===o?E(T).removeProp(r):T[r]=o,e[r]&&(e.jsonpCallback=t.jsonpCallback,nn.push(r)),i&&y(o)&&o(i[0]),i=o=void 0}),\"script\"}),b.createHTMLDocument=((tn=k.implementation.createHTMLDocument(\"\").body).innerHTML=\"

\",2===tn.childNodes.length),E.parseHTML=function(e,t,n){return\"string\"!=typeof e?[]:(\"boolean\"==typeof t&&(n=t,t=!1),t||(b.createHTMLDocument?((r=(t=k.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=k.location.href,t.head.appendChild(r)):t=k),i=!n&&[],(o=_.exec(e))?[t.createElement(o[1])]:(o=ye([e],t,i),i&&i.length&&E(i).remove(),E.merge([],o.childNodes)));var r,o,i},E.fn.load=function(e,t,n){var r,o,i,a=this,s=e.indexOf(\" \");return-1\").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,i||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,o,i,a,s,u,c=E.css(e,\"position\"),l=E(e),f={};\"static\"===c&&(e.style.position=\"relative\"),s=l.offset(),i=E.css(e,\"top\"),u=E.css(e,\"left\"),o=(\"absolute\"===c||\"fixed\"===c)&&-1<(i+u).indexOf(\"auto\")?(a=(r=l.position()).top,r.left):(a=parseFloat(i)||0,parseFloat(u)||0),y(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+o),\"using\"in t?t.using.call(e,f):(\"number\"==typeof f.top&&(f.top+=\"px\"),\"number\"==typeof f.left&&(f.left+=\"px\"),l.css(f))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],o={top:0,left:0};if(\"fixed\"===E.css(r,\"position\"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&\"static\"===E.css(e,\"position\");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((o=E(e).offset()).top+=E.css(e,\"borderTopWidth\",!0),o.left+=E.css(e,\"borderLeftWidth\",!0))}return{top:t.top-o.top-E.css(r,\"marginTop\",!0),left:t.left-o.left-E.css(r,\"marginLeft\",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&\"static\"===E.css(e,\"position\");)e=e.offsetParent;return e||re})}}),E.each({scrollLeft:\"pageXOffset\",scrollTop:\"pageYOffset\"},function(t,o){var i=\"pageYOffset\"===o;E.fn[t]=function(e){return $(this,function(e,t,n){var r;if(v(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[o]:e[t];r?r.scrollTo(i?r.pageXOffset:n,i?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each([\"top\",\"left\"],function(e,n){E.cssHooks[n]=Qe(b.pixelPosition,function(e,t){if(t)return t=Ke(e,n),Je.test(t)?E(e).position()[n]+\"px\":t})}),E.each({Height:\"height\",Width:\"width\"},function(a,s){E.each({padding:\"inner\"+a,content:s,\"\":\"outer\"+a},function(r,i){E.fn[i]=function(e,t){var n=arguments.length&&(r||\"boolean\"!=typeof e),o=r||(!0===e||!0===t?\"margin\":\"border\");return $(this,function(e,t,n){var r;return v(e)?0===i.indexOf(\"outer\")?e[\"inner\"+a]:e.document.documentElement[\"client\"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body[\"scroll\"+a],r[\"scroll\"+a],e.body[\"offset\"+a],r[\"offset\"+a],r[\"client\"+a])):void 0===n?E.css(e,t,o):E.style(e,t,n,o)},s,n?e:void 0,n)}})}),E.each([\"ajaxStart\",\"ajaxStop\",\"ajaxComplete\",\"ajaxError\",\"ajaxSuccess\",\"ajaxSend\"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,n){E.fn[n]=function(e,t){return 0e.length)&&e.substring(0,t.length)===t},vd:function(e,t){if(e===t)return!0;if(11===e.nodeType)return!1;if(t.contains)return t.contains(1!==e.nodeType?e.parentNode:e);if(t.compareDocumentPosition)return 16==(16&t.compareDocumentPosition(e));for(;e&&e!=t;)e=e.parentNode;return!!e},Sb:function(e){return _.a.vd(e,e.ownerDocument.documentElement)},kd:function(e){return!!_.a.Lb(e,_.a.Sb)},R:function(e){return e&&e.tagName&&e.tagName.toLowerCase()},Ac:function(e){return _.onError?function(){try{return e.apply(this,arguments)}catch(e){throw _.onError&&_.onError(e),e}}:e},setTimeout:(c=function(e,t){return setTimeout(_.a.Ac(e),t)},h.toString=function(){return c.toString()},h),Gc:function(e){setTimeout(function(){throw _.onError&&_.onError(e),e},0)},B:function(t,e,n){var r=_.a.Ac(n);if(n=l[e],_.options.useOnlyNativeEvents||n||!qe)if(n||\"function\"!=typeof t.addEventListener){if(void 0===t.attachEvent)throw Error(\"Browser doesn't support addEventListener or attachEvent\");var o=function(e){r.call(t,e)},i=\"on\"+e;t.attachEvent(i,o),_.a.K.za(t,function(){t.detachEvent(i,o)})}else t.addEventListener(e,r,!1);else u=u||(\"function\"==typeof qe(t).on?\"on\":\"bind\"),qe(t)[u](e,r)},Fb:function(e,t){if(!e||!e.nodeType)throw Error(\"element must be a DOM node when calling triggerEvent\");var n=!(\"input\"!==_.a.R(e)||!e.type||\"click\"!=t.toLowerCase())&&(\"checkbox\"==(n=e.type)||\"radio\"==n);if(_.options.useOnlyNativeEvents||!qe||n)if(\"function\"==typeof je.createEvent){if(\"function\"!=typeof e.dispatchEvent)throw Error(\"The supplied element doesn't support dispatchEvent\");(n=je.createEvent(s[t]||\"HTMLEvents\")).initEvent(t,!0,!0,Oe,0,0,0,0,0,!1,!1,!1,!1,0,e),e.dispatchEvent(n)}else if(n&&e.click)e.click();else{if(void 0===e.fireEvent)throw Error(\"Browser doesn't support triggering events\");e.fireEvent(\"on\"+t)}else qe(e).trigger(t)},f:function(e){return _.O(e)?e():e},bc:function(e){return _.O(e)?e.v():e},Eb:function(t,e,n){var r;e&&(\"object\"===_typeof(t.classList)?(r=t.classList[n?\"add\":\"remove\"],_.a.D(e.match(p),function(e){r.call(t.classList,e)})):\"string\"==typeof t.className.baseVal?o(t.className,\"baseVal\",e,n):o(t,\"className\",e,n))},Bb:function(e,t){var n=_.a.f(t);null!==n&&n!==De||(n=\"\");var r=_.h.firstChild(e);!r||3!=r.nodeType||_.h.nextSibling(r)?_.h.va(e,[e.ownerDocument.createTextNode(n)]):r.data=n,_.a.Ad(e)},Yc:function(e,t){if(e.name=t,d<=7)try{var n=e.name.replace(/[&<>'\"]/g,function(e){return\"&#\"+e.charCodeAt(0)+\";\"});e.mergeAttributes(je.createElement(\"\"),!1)}catch(e){}},Ad:function(e){9<=d&&((e=1==e.nodeType?e:e.parentNode).style&&(e.style.zoom=e.style.zoom))},wd:function(e){var t;d&&(t=e.style.width,e.style.width=0,e.style.width=t)},Pd:function(e,t){e=_.a.f(e),t=_.a.f(t);for(var n=[],r=e;r<=t;r++)n.push(r);return n},la:function(e){for(var t=[],n=0,r=e.length;n\",\"\"],tbody:t,tfoot:t,tr:[2,\"\",\"
\"],td:l=[3,\"\",\"
\"],th:l,option:f=[1,\"\"],optgroup:f},p=_.a.W<=8,_.a.ua=function(e,t){var n;if(qe){if(qe.parseHTML)n=qe.parseHTML(e,t)||[];else if((n=qe.clean([e],t))&&n[0]){for(var r=n[0];r.parentNode&&11!==r.parentNode.nodeType;)r=r.parentNode;r.parentNode&&r.parentNode.removeChild(r)}}else{(n=t)||(n=je);var r=n.parentWindow||n.defaultView||Oe,o=_.a.Db(e).toLowerCase(),i=n.createElement(\"div\"),a=(o=o.match(/^(?:\\x3c!--.*?--\\x3e\\s*?)*?<([a-z]+)[\\s>]/))&&d[o[1]]||u,o=a[0];for(a=\"ignored
\"+a[1]+e+a[2]+\"
\",\"function\"==typeof r.innerShiv?i.appendChild(r.innerShiv(a)):(p&&n.body.appendChild(i),i.innerHTML=a,p&&i.parentNode.removeChild(i));o--;)i=i.lastChild;n=_.a.la(i.lastChild.childNodes)}return n},_.a.Md=function(e,t){var n=_.a.ua(e,t);return n.length&&n[0].parentElement||_.a.Yb(n)},_.a.fc=function(e,t){if(_.a.Tb(e),null!==(t=_.a.f(t))&&t!==De)if(\"string\"!=typeof t&&(t=t.toString()),qe)qe(e).html(t);else for(var n=_.a.ua(t,e.ownerDocument),r=0;r]*))?)*\\s+)data-bind\\s*=\\s*([\"'])([\\s\\S]*?)\\3/gi,Te=/\\x3c!--\\s*ko\\b\\s*([\\s\\S]*?)\\s*--\\x3e/g,{xd:function(e,t,n){t.isTemplateRewritten(e,n)||t.rewriteTemplate(e,function(e){return _.kc.Ld(e,t)},n)},Ld:function(e,i){return e.replace(Ce,function(e,t,n,r,o){return Ne(o,t,n,i)}).replace(Te,function(e,t){return Ne(t,\"\\x3c!-- ko --\\x3e\",\"#comment\",i)})},md:function(r,o){return _.aa.Xb(function(e,t){var n=e.nextSibling;n&&n.nodeName.toLowerCase()===o&&_.ib(n,r,t)})}}),_.b(\"__tr_ambtns\",_.kc.md),function(){_.C={},_.C.F=function(e){var t;(this.F=e)&&(t=_.a.R(e),this.ab=\"script\"===t?1:\"textarea\"===t?2:\"template\"==t&&e.content&&11===e.content.nodeType?3:4)},_.C.F.prototype.text=function(){var e=1===this.ab?\"text\":2===this.ab?\"value\":\"innerHTML\";if(0==arguments.length)return this.F[e];var t=arguments[0];\"innerHTML\"==e?_.a.fc(this.F,t):this.F[e]=t};var t=_.a.g.Z()+\"_\";_.C.F.prototype.data=function(e){if(1===arguments.length)return _.a.g.get(this.F,t+e);_.a.g.set(this.F,t+e,arguments[1])};var o=_.a.g.Z();_.C.F.prototype.nodes=function(){var e=this.F;if(0==arguments.length){var t,n=_.a.g.get(e,o)||{},r=n.lb||(3===this.ab?e.content:4===this.ab?e:De);return r&&!n.jd||(t=this.text())&&t!==n.bb&&(r=_.a.Md(t,e.ownerDocument),_.a.g.set(e,o,{lb:r,bb:t,jd:!0})),r}n=arguments[0],this.ab!==De&&this.text(\"\"),_.a.g.set(e,o,{lb:n})},_.C.ia=function(e){this.F=e},_.C.ia.prototype=new _.C.F,_.C.ia.prototype.constructor=_.C.ia,_.C.ia.prototype.text=function(){if(0==arguments.length){var e=_.a.g.get(this.F,o)||{};return e.bb===De&&e.lb&&(e.bb=e.lb.innerHTML),e.bb}_.a.g.set(this.F,o,{bb:arguments[0]})},_.b(\"templateSources\",_.C),_.b(\"templateSources.domElement\",_.C.F),_.b(\"templateSources.anonymousTemplate\",_.C.ia)}(),function(){function r(e,t,n){var r;for(t=_.h.nextSibling(t);e&&(r=e)!==t;)n(r,e=_.h.nextSibling(r))}function d(e,t){if(e.length){var o=e[0],i=e[e.length-1],n=o.parentNode,a=_.ga.instance,s=a.preprocessNode;if(s){if(r(o,i,function(e,t){var n=e.previousSibling,r=s.call(a,e);r&&(e===o&&(o=r[0]||t),e===i&&(i=r[r.length-1]||n))}),e.length=0,!o)return;o===i?e.push(o):(e.push(o,i),_.a.Ua(e,n))}r(o,i,function(e){1!==e.nodeType&&8!==e.nodeType||_.vc(t,e)}),r(o,i,function(e){1!==e.nodeType&&8!==e.nodeType||_.aa.cd(e,[t])}),_.a.Ua(e,n)}}function u(e){return e.nodeType?e:0\"+t+\"<\\/script>\")},0>10|55296,1023&n|56320))}function o(){C()}var e,p,w,i,a,h,d,v,x,u,c,C,T,s,k,g,l,m,b,E=\"sizzle\"+ +new Date,y=n.document,S=0,r=0,N=ue(),_=ue(),A=ue(),D=ue(),O=function(e,t){return e===t&&(c=!0),0},j={}.hasOwnProperty,t=[],M=t.pop,q=t.push,L=t.push,P=t.slice,R=function(e,t){for(var n=0,r=e.length;n+~]|\"+B+\")\"+B+\"*\"),z=new RegExp(B+\"|>\"),G=new RegExp($),X=new RegExp(\"^\"+H+\"$\"),Y={ID:new RegExp(\"^#(\"+H+\")\"),CLASS:new RegExp(\"^\\\\.(\"+H+\")\"),TAG:new RegExp(\"^(\"+H+\"|[*])\"),ATTR:new RegExp(\"^\"+F),PSEUDO:new RegExp(\"^\"+$),CHILD:new RegExp(\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\"+B+\"*(even|odd|(([+-]|)(\\\\d*)n|)\"+B+\"*(?:([+-]|)\"+B+\"*(\\\\d+)|))\"+B+\"*\\\\)|)\",\"i\"),bool:new RegExp(\"^(?:\"+I+\")$\",\"i\"),needsContext:new RegExp(\"^\"+B+\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\"+B+\"*((?:-\\\\d)?\\\\d*)\"+B+\"*\\\\)|)(?=[^-]|$)\",\"i\")},K=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,Z=/^h\\d$/i,ee=/^[^{]+\\{\\s*\\[native \\w/,te=/^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,ne=/[+~]/,re=new RegExp(\"\\\\\\\\[\\\\da-fA-F]{1,6}\"+B+\"?|\\\\\\\\([^\\\\r\\\\n\\\\f])\",\"g\"),oe=/([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,ie=function(e,t){return t?\"\\0\"===e?\"�\":e.slice(0,-1)+\"\\\\\"+e.charCodeAt(e.length-1).toString(16)+\" \":\"\\\\\"+e},ae=be(function(e){return!0===e.disabled&&\"fieldset\"===e.nodeName.toLowerCase()},{dir:\"parentNode\",next:\"legend\"});try{L.apply(t=P.call(y.childNodes),y.childNodes),t[y.childNodes.length].nodeType}catch(e){L={apply:t.length?function(e,t){q.apply(e,P.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function se(t,e,n,r){var o,i,a,s,u,c,l,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],\"string\"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(C(e),e=e||T,k)){if(11!==d&&(u=te.exec(t)))if(o=u[1]){if(9===d){if(!(a=e.getElementById(o)))return n;if(a.id===o)return n.push(a),n}else if(f&&(a=f.getElementById(o))&&b(e,a)&&a.id===o)return n.push(a),n}else{if(u[2])return L.apply(n,e.getElementsByTagName(t)),n;if((o=u[3])&&p.getElementsByClassName&&e.getElementsByClassName)return L.apply(n,e.getElementsByClassName(o)),n}if(p.qsa&&!D[t+\" \"]&&(!g||!g.test(t))&&(1!==d||\"object\"!==e.nodeName.toLowerCase())){if(l=t,f=e,1===d&&(z.test(t)||J.test(t))){for((f=ne.test(t)&&ve(e.parentNode)||e)===e&&p.scope||((s=e.getAttribute(\"id\"))?s=s.replace(oe,ie):e.setAttribute(\"id\",s=E)),i=(c=h(t)).length;i--;)c[i]=(s?\"#\"+s:\":scope\")+\" \"+me(c[i]);l=c.join(\",\")}try{return L.apply(n,f.querySelectorAll(l)),n}catch(e){D(t,!0)}finally{s===E&&e.removeAttribute(\"id\")}}}return v(t.replace(V,\"$1\"),e,n,r)}function ue(){var n=[];function r(e,t){return n.push(e+\" \")>w.cacheLength&&delete r[n.shift()],r[e+\" \"]=t}return r}function ce(e){return e[E]=!0,e}function le(e){var t=T.createElement(\"fieldset\");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){for(var n=e.split(\"|\"),r=n.length;r--;)w.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return\"form\"in e?e.parentNode&&!1===e.disabled?\"label\"in e?\"label\"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:\"label\"in e&&e.disabled===t}}function he(a){return ce(function(i){return i=+i,ce(function(e,t){for(var n,r=a([],e.length,i),o=r.length;o--;)e[n=r[o]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in p=se.support={},a=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!K.test(t||n&&n.nodeName||\"HTML\")},C=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:y;return r!=T&&9===r.nodeType&&r.documentElement&&(s=(T=r).documentElement,k=!a(T),y!=T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener(\"unload\",o,!1):n.attachEvent&&n.attachEvent(\"onunload\",o)),p.scope=le(function(e){return s.appendChild(e).appendChild(T.createElement(\"div\")),void 0!==e.querySelectorAll&&!e.querySelectorAll(\":scope fieldset div\").length}),p.attributes=le(function(e){return e.className=\"i\",!e.getAttribute(\"className\")}),p.getElementsByTagName=le(function(e){return e.appendChild(T.createComment(\"\")),!e.getElementsByTagName(\"*\").length}),p.getElementsByClassName=ee.test(T.getElementsByClassName),p.getById=le(function(e){return s.appendChild(e).id=E,!T.getElementsByName||!T.getElementsByName(E).length}),p.getById?(w.filter.ID=function(e){var t=e.replace(re,f);return function(e){return e.getAttribute(\"id\")===t}},w.find.ID=function(e,t){if(void 0!==t.getElementById&&k){var n=t.getElementById(e);return n?[n]:[]}}):(w.filter.ID=function(e){var n=e.replace(re,f);return function(e){var t=void 0!==e.getAttributeNode&&e.getAttributeNode(\"id\");return t&&t.value===n}},w.find.ID=function(e,t){if(void 0!==t.getElementById&&k){var n,r,o,i=t.getElementById(e);if(i){if((n=i.getAttributeNode(\"id\"))&&n.value===e)return[i];for(o=t.getElementsByName(e),r=0;i=o[r++];)if((n=i.getAttributeNode(\"id\"))&&n.value===e)return[i]}return[]}}),w.find.TAG=p.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if(\"*\"!==e)return i;for(;n=i[o++];)1===n.nodeType&&r.push(n);return r},w.find.CLASS=p.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&k)return t.getElementsByClassName(e)},l=[],g=[],(p.qsa=ee.test(T.querySelectorAll))&&(le(function(e){var t;s.appendChild(e).innerHTML=\"\",e.querySelectorAll(\"[msallowcapture^='']\").length&&g.push(\"[*^$]=\"+B+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\"[selected]\").length||g.push(\"\\\\[\"+B+\"*(?:value|\"+I+\")\"),e.querySelectorAll(\"[id~=\"+E+\"-]\").length||g.push(\"~=\"),(t=T.createElement(\"input\")).setAttribute(\"name\",\"\"),e.appendChild(t),e.querySelectorAll(\"[name='']\").length||g.push(\"\\\\[\"+B+\"*name\"+B+\"*=\"+B+\"*(?:''|\\\"\\\")\"),e.querySelectorAll(\":checked\").length||g.push(\":checked\"),e.querySelectorAll(\"a#\"+E+\"+*\").length||g.push(\".#.+[+~]\"),e.querySelectorAll(\"\\\\\\f\"),g.push(\"[\\\\r\\\\n\\\\f]\")}),le(function(e){e.innerHTML=\"\";var t=T.createElement(\"input\");t.setAttribute(\"type\",\"hidden\"),e.appendChild(t).setAttribute(\"name\",\"D\"),e.querySelectorAll(\"[name=d]\").length&&g.push(\"name\"+B+\"*[*^$|!~]?=\"),2!==e.querySelectorAll(\":enabled\").length&&g.push(\":enabled\",\":disabled\"),s.appendChild(e).disabled=!0,2!==e.querySelectorAll(\":disabled\").length&&g.push(\":enabled\",\":disabled\"),e.querySelectorAll(\"*,:x\"),g.push(\",.*:\")})),(p.matchesSelector=ee.test(m=s.matches||s.webkitMatchesSelector||s.mozMatchesSelector||s.oMatchesSelector||s.msMatchesSelector))&&le(function(e){p.disconnectedMatch=m.call(e,\"*\"),m.call(e,\"[s!='']:x\"),l.push(\"!=\",$)}),g=g.length&&new RegExp(g.join(\"|\")),l=l.length&&new RegExp(l.join(\"|\")),t=ee.test(s.compareDocumentPosition),b=t||ee.test(s.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},O=t?function(e,t){if(e===t)return c=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==y&&b(y,e)?-1:t==T||t.ownerDocument==y&&b(y,t)?1:u?R(u,e)-R(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return c=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],s=[t];if(!o||!i)return e==T?-1:t==T?1:o?-1:i?1:u?R(u,e)-R(u,t):0;if(o===i)return de(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?de(a[r],s[r]):a[r]==y?-1:s[r]==y?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),p.matchesSelector&&k&&!D[t+\" \"]&&(!l||!l.test(t))&&(!g||!g.test(t)))try{var n=m.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){D(t,!0)}return 0\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(re,f),e[3]=(e[3]||e[4]||e[5]||\"\").replace(re,f),\"~=\"===e[2]&&(e[3]=\" \"+e[3]+\" \"),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),\"nth\"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*(\"even\"===e[3]||\"odd\"===e[3])),e[5]=+(e[7]+e[8]||\"odd\"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||\"\":n&&G.test(n)&&(t=h(n,!0))&&(t=n.indexOf(\")\",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(re,f).toLowerCase();return\"*\"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+\" \"];return t||(t=new RegExp(\"(^|\"+B+\")\"+e+\"(\"+B+\"|$)\"))&&N(e,function(e){return t.test(\"string\"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute(\"class\")||\"\")})},ATTR:function(n,r,o){return function(e){var t=se.attr(e,n);return null==t?\"!=\"===r:!r||(t+=\"\",\"=\"===r?t===o:\"!=\"===r?t!==o:\"^=\"===r?o&&0===t.indexOf(o):\"*=\"===r?o&&-1\",\"#\"===e.firstChild.getAttribute(\"href\")})||fe(\"type|href|height|width\",function(e,t,n){if(!n)return e.getAttribute(t,\"type\"===t.toLowerCase()?1:2)}),p.attributes&&le(function(e){return e.innerHTML=\"\",e.firstChild.setAttribute(\"value\",\"\"),\"\"===e.firstChild.getAttribute(\"value\")})||fe(\"value\",function(e,t,n){if(!n&&\"input\"===e.nodeName.toLowerCase())return e.defaultValue}),le(function(e){return null==e.getAttribute(\"disabled\")})||fe(I,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(T);E.find=p,E.expr=p.selectors,E.expr[\":\"]=E.expr.pseudos,E.uniqueSort=E.unique=p.uniqueSort,E.text=p.getText,E.isXMLDoc=p.isXML,E.contains=p.contains,E.escapeSelector=p.escape;function h(e,t,n){for(var r=[],o=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(o&&E(e).is(n))break;r.push(e)}return r}function C(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}var S=E.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var _=/^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;function A(e,n,r){return y(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):\"string\"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\\w-]+))$/;(E.fn.init=function(e,t,n){var r,o;if(!e)return this;if(n=n||D,\"string\"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):y(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this);if(!(r=\"<\"===e[0]&&\">\"===e[e.length-1]&&3<=e.length?[null,e,null]:O.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:k,!0)),_.test(r[1])&&E.isPlainObject(t))for(r in t)y(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(o=k.getElementById(r[2]))&&(this[0]=o,this.length=1),this}).prototype=E.fn,D=E(k);var j=/^(?:parents|prev(?:Until|All))/,M={children:!0,contents:!0,next:!0,prev:!0};function q(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\\x20\\t\\r\\n\\f]*)/i,he=/^$|^module$|\\/(?:java|ecma)script/i;le=k.createDocumentFragment().appendChild(k.createElement(\"div\")),(fe=k.createElement(\"input\")).setAttribute(\"type\",\"radio\"),fe.setAttribute(\"checked\",\"checked\"),fe.setAttribute(\"name\",\"t\"),le.appendChild(fe),b.checkClone=le.cloneNode(!0).cloneNode(!0).lastChild.checked,le.innerHTML=\"\",b.noCloneChecked=!!le.cloneNode(!0).lastChild.defaultValue,le.innerHTML=\"\",b.option=!!le.lastChild;var ve={thead:[1,\"\",\"
\"],col:[2,\"\",\"
\"],tr:[2,\"\",\"
\"],td:[3,\"\",\"
\"],_default:[0,\"\",\"\"]};function ge(e,t){var n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||\"*\"):void 0!==e.querySelectorAll?e.querySelectorAll(t||\"*\"):[];return void 0===t||t&&N(e,t)?E.merge([e],n):n}function me(e,t){for(var n=0,r=e.length;n\",\"\"]);var be=/<|&#?\\w+;/;function ye(e,t,n,r,o){for(var i,a,s,u,c,l,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\\s*$/g;function Oe(e,t){return N(e,\"table\")&&N(11!==t.nodeType?t:t.firstChild,\"tr\")&&E(e).children(\"tbody\")[0]||e}function je(e){return e.type=(null!==e.getAttribute(\"type\"))+\"/\"+e.type,e}function Me(e){return\"true/\"===(e.type||\"\").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute(\"type\"),e}function qe(e,t){var n,r,o,i,a,s;if(1===t.nodeType){if(X.hasData(e)&&(s=X.get(e).events))for(o in X.remove(t,\"handle events\"),s)for(n=0,r=s[o].length;n\").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on(\"load error\",o=function(e){r.remove(),o=null,e&&t(\"error\"===e.type?404:200,e.type)}),k.head.appendChild(r[0])},abort:function(){o&&o()}}});var en,tn=[],nn=/(=)\\?(?=&|$)|\\?\\?/;E.ajaxSetup({jsonp:\"callback\",jsonpCallback:function(){var e=tn.pop()||E.expando+\"_\"+Mt.guid++;return this[e]=!0,e}}),E.ajaxPrefilter(\"json jsonp\",function(e,t,n){var r,o,i,a=!1!==e.jsonp&&(nn.test(e.url)?\"url\":\"string\"==typeof e.data&&0===(e.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&nn.test(e.data)&&\"data\");if(a||\"jsonp\"===e.dataTypes[0])return r=e.jsonpCallback=y(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(nn,\"$1\"+r):!1!==e.jsonp&&(e.url+=(qt.test(e.url)?\"&\":\"?\")+e.jsonp+\"=\"+r),e.converters[\"script json\"]=function(){return i||E.error(r+\" was not called\"),i[0]},e.dataTypes[0]=\"json\",o=T[r],T[r]=function(){i=arguments},n.always(function(){void 0===o?E(T).removeProp(r):T[r]=o,e[r]&&(e.jsonpCallback=t.jsonpCallback,tn.push(r)),i&&y(o)&&o(i[0]),i=o=void 0}),\"script\"}),b.createHTMLDocument=((en=k.implementation.createHTMLDocument(\"\").body).innerHTML=\"
\",2===en.childNodes.length),E.parseHTML=function(e,t,n){return\"string\"!=typeof e?[]:(\"boolean\"==typeof t&&(n=t,t=!1),t||(b.createHTMLDocument?((r=(t=k.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=k.location.href,t.head.appendChild(r)):t=k),i=!n&&[],(o=_.exec(e))?[t.createElement(o[1])]:(o=ye([e],t,i),i&&i.length&&E(i).remove(),E.merge([],o.childNodes)));var r,o,i},E.fn.load=function(e,t,n){var r,o,i,a=this,s=e.indexOf(\" \");return-1\").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,i||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,o,i,a,s,u,c=E.css(e,\"position\"),l=E(e),f={};\"static\"===c&&(e.style.position=\"relative\"),s=l.offset(),i=E.css(e,\"top\"),u=E.css(e,\"left\"),o=(\"absolute\"===c||\"fixed\"===c)&&-1<(i+u).indexOf(\"auto\")?(a=(r=l.position()).top,r.left):(a=parseFloat(i)||0,parseFloat(u)||0),y(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+o),\"using\"in t?t.using.call(e,f):(\"number\"==typeof f.top&&(f.top+=\"px\"),\"number\"==typeof f.left&&(f.left+=\"px\"),l.css(f))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],o={top:0,left:0};if(\"fixed\"===E.css(r,\"position\"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&\"static\"===E.css(e,\"position\");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((o=E(e).offset()).top+=E.css(e,\"borderTopWidth\",!0),o.left+=E.css(e,\"borderLeftWidth\",!0))}return{top:t.top-o.top-E.css(r,\"marginTop\",!0),left:t.left-o.left-E.css(r,\"marginLeft\",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&\"static\"===E.css(e,\"position\");)e=e.offsetParent;return e||re})}}),E.each({scrollLeft:\"pageXOffset\",scrollTop:\"pageYOffset\"},function(t,o){var i=\"pageYOffset\"===o;E.fn[t]=function(e){return $(this,function(e,t,n){var r;return v(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n?r?r[o]:e[t]:void(r?r.scrollTo(i?r.pageXOffset:n,i?n:r.pageYOffset):e[t]=n)},t,e,arguments.length)}}),E.each([\"top\",\"left\"],function(e,n){E.cssHooks[n]=Qe(b.pixelPosition,function(e,t){if(t)return t=Ke(e,n),Je.test(t)?E(e).position()[n]+\"px\":t})}),E.each({Height:\"height\",Width:\"width\"},function(a,s){E.each({padding:\"inner\"+a,content:s,\"\":\"outer\"+a},function(r,i){E.fn[i]=function(e,t){var n=arguments.length&&(r||\"boolean\"!=typeof e),o=r||(!0===e||!0===t?\"margin\":\"border\");return $(this,function(e,t,n){var r;return v(e)?0===i.indexOf(\"outer\")?e[\"inner\"+a]:e.document.documentElement[\"client\"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body[\"scroll\"+a],r[\"scroll\"+a],e.body[\"offset\"+a],r[\"offset\"+a],r[\"client\"+a])):void 0===n?E.css(e,t,o):E.style(e,t,n,o)},s,n?e:void 0,n)}})}),E.each([\"ajaxStart\",\"ajaxStop\",\"ajaxComplete\",\"ajaxError\",\"ajaxSuccess\",\"ajaxSend\"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,n){E.fn[n]=function(e,t){return 0e.length)&&e.substring(0,t.length)===t},vd:function(e,t){if(e===t)return!0;if(11===e.nodeType)return!1;if(t.contains)return t.contains(1!==e.nodeType?e.parentNode:e);if(t.compareDocumentPosition)return 16==(16&t.compareDocumentPosition(e));for(;e&&e!=t;)e=e.parentNode;return!!e},Sb:function(e){return _.a.vd(e,e.ownerDocument.documentElement)},kd:function(e){return!!_.a.Lb(e,_.a.Sb)},R:function(e){return e&&e.tagName&&e.tagName.toLowerCase()},Ac:function(e){return _.onError?function(){try{return e.apply(this,arguments)}catch(e){throw _.onError&&_.onError(e),e}}:e},setTimeout:(c=function(e,t){return setTimeout(_.a.Ac(e),t)},h.toString=function(){return c.toString()},h),Gc:function(e){setTimeout(function(){throw _.onError&&_.onError(e),e},0)},B:function(t,e,n){var r=_.a.Ac(n);if(n=l[e],_.options.useOnlyNativeEvents||n||!Me)if(n||\"function\"!=typeof t.addEventListener){if(void 0===t.attachEvent)throw Error(\"Browser doesn't support addEventListener or attachEvent\");var o=function(e){r.call(t,e)},i=\"on\"+e;t.attachEvent(i,o),_.a.K.za(t,function(){t.detachEvent(i,o)})}else t.addEventListener(e,r,!1);else u=u||(\"function\"==typeof Me(t).on?\"on\":\"bind\"),Me(t)[u](e,r)},Fb:function(e,t){if(!e||!e.nodeType)throw Error(\"element must be a DOM node when calling triggerEvent\");var n=!(\"input\"!==_.a.R(e)||!e.type||\"click\"!=t.toLowerCase())&&(\"checkbox\"==(n=e.type)||\"radio\"==n);if(_.options.useOnlyNativeEvents||!Me||n)if(\"function\"==typeof Oe.createEvent){if(\"function\"!=typeof e.dispatchEvent)throw Error(\"The supplied element doesn't support dispatchEvent\");(n=Oe.createEvent(s[t]||\"HTMLEvents\")).initEvent(t,!0,!0,De,0,0,0,0,0,!1,!1,!1,!1,0,e),e.dispatchEvent(n)}else if(n&&e.click)e.click();else{if(void 0===e.fireEvent)throw Error(\"Browser doesn't support triggering events\");e.fireEvent(\"on\"+t)}else Me(e).trigger(t)},f:function(e){return _.O(e)?e():e},bc:function(e){return _.O(e)?e.v():e},Eb:function(t,e,n){var r;e&&(\"object\"===_typeof(t.classList)?(r=t.classList[n?\"add\":\"remove\"],_.a.D(e.match(p),function(e){r.call(t.classList,e)})):\"string\"==typeof t.className.baseVal?o(t.className,\"baseVal\",e,n):o(t,\"className\",e,n))},Bb:function(e,t){var n=_.a.f(t);null!==n&&n!==Ae||(n=\"\");var r=_.h.firstChild(e);!r||3!=r.nodeType||_.h.nextSibling(r)?_.h.va(e,[e.ownerDocument.createTextNode(n)]):r.data=n,_.a.Ad(e)},Yc:function(e,t){if(e.name=t,d<=7)try{var n=e.name.replace(/[&<>'\"]/g,function(e){return\"&#\"+e.charCodeAt(0)+\";\"});e.mergeAttributes(Oe.createElement(\"\"),!1)}catch(e){}},Ad:function(e){9<=d&&((e=1==e.nodeType?e:e.parentNode).style&&(e.style.zoom=e.style.zoom))},wd:function(e){var t;d&&(t=e.style.width,e.style.width=0,e.style.width=t)},Pd:function(e,t){e=_.a.f(e),t=_.a.f(t);for(var n=[],r=e;r<=t;r++)n.push(r);return n},la:function(e){for(var t=[],n=0,r=e.length;n\",\"\"],tbody:t,tfoot:t,tr:[2,\"\",\"
\"],td:l=[3,\"\",\"
\"],th:l,option:f=[1,\"\"],optgroup:f},p=_.a.W<=8,_.a.ua=function(e,t){var n;if(Me){if(Me.parseHTML)n=Me.parseHTML(e,t)||[];else if((n=Me.clean([e],t))&&n[0]){for(var r=n[0];r.parentNode&&11!==r.parentNode.nodeType;)r=r.parentNode;r.parentNode&&r.parentNode.removeChild(r)}}else{(n=t)||(n=Oe);var r=n.parentWindow||n.defaultView||De,o=_.a.Db(e).toLowerCase(),i=n.createElement(\"div\"),a=(o=o.match(/^(?:\\x3c!--.*?--\\x3e\\s*?)*?<([a-z]+)[\\s>]/))&&d[o[1]]||u,o=a[0];for(a=\"ignored
\"+a[1]+e+a[2]+\"
\",\"function\"==typeof r.innerShiv?i.appendChild(r.innerShiv(a)):(p&&n.body.appendChild(i),i.innerHTML=a,p&&i.parentNode.removeChild(i));o--;)i=i.lastChild;n=_.a.la(i.lastChild.childNodes)}return n},_.a.Md=function(e,t){var n=_.a.ua(e,t);return n.length&&n[0].parentElement||_.a.Yb(n)},_.a.fc=function(e,t){if(_.a.Tb(e),null!==(t=_.a.f(t))&&t!==Ae)if(\"string\"!=typeof t&&(t=t.toString()),Me)Me(e).html(t);else for(var n=_.a.ua(t,e.ownerDocument),r=0;r]*))?)*\\s+)data-bind\\s*=\\s*([\"'])([\\s\\S]*?)\\3/gi,Te=/\\x3c!--\\s*ko\\b\\s*([\\s\\S]*?)\\s*--\\x3e/g,{xd:function(e,t,n){t.isTemplateRewritten(e,n)||t.rewriteTemplate(e,function(e){return _.kc.Ld(e,t)},n)},Ld:function(e,i){return e.replace(Ce,function(e,t,n,r,o){return Ne(o,t,n,i)}).replace(Te,function(e,t){return Ne(t,\"\\x3c!-- ko --\\x3e\",\"#comment\",i)})},md:function(r,o){return _.aa.Xb(function(e,t){var n=e.nextSibling;n&&n.nodeName.toLowerCase()===o&&_.ib(n,r,t)})}}),_.b(\"__tr_ambtns\",_.kc.md),function(){_.C={},_.C.F=function(e){var t;(this.F=e)&&(t=_.a.R(e),this.ab=\"script\"===t?1:\"textarea\"===t?2:\"template\"==t&&e.content&&11===e.content.nodeType?3:4)},_.C.F.prototype.text=function(){var e=1===this.ab?\"text\":2===this.ab?\"value\":\"innerHTML\";if(0==arguments.length)return this.F[e];var t=arguments[0];\"innerHTML\"==e?_.a.fc(this.F,t):this.F[e]=t};var t=_.a.g.Z()+\"_\";_.C.F.prototype.data=function(e){if(1===arguments.length)return _.a.g.get(this.F,t+e);_.a.g.set(this.F,t+e,arguments[1])};var o=_.a.g.Z();_.C.F.prototype.nodes=function(){var e=this.F;if(0==arguments.length){var t,n=_.a.g.get(e,o)||{},r=n.lb||(3===this.ab?e.content:4===this.ab?e:Ae);return r&&!n.jd||(t=this.text())&&t!==n.bb&&(r=_.a.Md(t,e.ownerDocument),_.a.g.set(e,o,{lb:r,bb:t,jd:!0})),r}n=arguments[0],this.ab!==Ae&&this.text(\"\"),_.a.g.set(e,o,{lb:n})},_.C.ia=function(e){this.F=e},_.C.ia.prototype=new _.C.F,_.C.ia.prototype.constructor=_.C.ia,_.C.ia.prototype.text=function(){if(0==arguments.length){var e=_.a.g.get(this.F,o)||{};return e.bb===Ae&&e.lb&&(e.bb=e.lb.innerHTML),e.bb}_.a.g.set(this.F,o,{bb:arguments[0]})},_.b(\"templateSources\",_.C),_.b(\"templateSources.domElement\",_.C.F),_.b(\"templateSources.anonymousTemplate\",_.C.ia)}(),function(){function r(e,t,n){var r;for(t=_.h.nextSibling(t);e&&(r=e)!==t;)n(r,e=_.h.nextSibling(r))}function d(e,t){if(e.length){var o=e[0],i=e[e.length-1],n=o.parentNode,a=_.ga.instance,s=a.preprocessNode;if(s){if(r(o,i,function(e,t){var n=e.previousSibling,r=s.call(a,e);r&&(e===o&&(o=r[0]||t),e===i&&(i=r[r.length-1]||n))}),e.length=0,!o)return;o===i?e.push(o):(e.push(o,i),_.a.Ua(e,n))}r(o,i,function(e){1!==e.nodeType&&8!==e.nodeType||_.vc(t,e)}),r(o,i,function(e){1!==e.nodeType&&8!==e.nodeType||_.aa.cd(e,[t])}),_.a.Ua(e,n)}}function u(e){return e.nodeType?e:0\"+t+\"<\\/script>\")},0 Date: Tue, 23 Jun 2020 23:56:34 +0100 Subject: [PATCH 09/18] Warning fixes --- src/mqtt.cpp | 4 ++-- src/profile.h | 2 +- src/web_server.cpp | 2 ++ src/wifi.cpp | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 220fdc1..0abbc14 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -57,7 +57,7 @@ void mqttmsg_callback(char *topic, byte * payload, unsigned int length) { } else if (topic_string == mqtt_vrms){ voltage = payload_str.toFloat(); - DBUGF("voltage:%d", voltage); + DBUGF("voltage: %.2f", voltage); OpenEVSE.setVoltage(voltage, [](int ret) { // Only gives better power calculations so not critical if this fails }); @@ -84,7 +84,7 @@ void mqttmsg_callback(char *topic, byte * payload, unsigned int length) { if (payload[0] != 0); { // If MQTT msg contains a payload e.g $SC 13. Not all rapi commands have a payload e.g. $GC cmd += " "; // print RAPI value received via MQTT serial - for (int i = 0; i < length; i++) { + for (unsigned int i = 0; i < length; i++) { cmd += (char)payload[i]; } } diff --git a/src/profile.h b/src/profile.h index 101c70f..4e8a81a 100644 --- a/src/profile.h +++ b/src/profile.h @@ -9,7 +9,7 @@ #define Profile_End(x, max) \ unsigned long profile ## x ## Diff = millis() - profile ## x; \ if(profile ## x ## Diff > max) { \ - DBUGF(">> Slow " #x " %dms", profile ## x ## Diff);\ + DBUGF(">> Slow " #x " %lums", profile ## x ## Diff);\ } #else // ENABLE_PROFILE diff --git a/src/web_server.cpp b/src/web_server.cpp index 11a68ee..63b2e0b 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -47,6 +47,7 @@ const char _CONTENT_TYPE_SVG[] PROGMEM = "image/svg+xml"; String currentfirmware = ESCAPEQUOTE(BUILD_TAG); void dumpRequest(AsyncWebServerRequest *request) { +#ifdef ENABLE_DEBUG if(request->method() == HTTP_GET) { DBUGF("GET"); } else if(request->method() == HTTP_POST) { @@ -89,6 +90,7 @@ void dumpRequest(AsyncWebServerRequest *request) { DBUGF("_GET[%s]: %s", p->name().c_str(), p->value().c_str()); } } +#endif } // ------------------------------------------------------------------- diff --git a/src/wifi.cpp b/src/wifi.cpp index 4de1a71..f0a7b56 100644 --- a/src/wifi.cpp +++ b/src/wifi.cpp @@ -243,9 +243,9 @@ wifi_loop() { Profile_Start(wifi_loop); - bool isClient = wifi_mode_is_sta(); +// bool isClient = wifi_mode_is_sta(); bool isClientOnly = wifi_mode_is_sta_only(); - bool isAp = wifi_mode_is_ap(); +// bool isAp = wifi_mode_is_ap(); bool isApOnly = wifi_mode_is_ap_only(); #ifdef WIFI_LED From 164009802a37d0bdc5d3d0df55e24a7114a7a734 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Wed, 24 Jun 2020 00:03:56 +0100 Subject: [PATCH 10/18] Fixes to display LCD messages during OTA --- src/lcd.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lcd.cpp b/src/lcd.cpp index 9383313..4eecc68 100644 --- a/src/lcd.cpp +++ b/src/lcd.cpp @@ -54,6 +54,7 @@ void lcd_display(Message *msg, int x, int y, int time, uint32_t flags) if(flags & LCD_DISPLAY_NOW) { lcd_loop(); + rapiSender.flush(); } } From 7eee8716639eb18359172ea2a45567dfd36607d9 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Wed, 24 Jun 2020 00:04:10 +0100 Subject: [PATCH 11/18] Removed redundant code --- src/ota.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ota.cpp b/src/ota.cpp index 7c01b90..2e7a421 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -17,9 +17,6 @@ void ota_setup() ArduinoOTA.begin(); ArduinoOTA.onStart([]() { - // Clean SPIFFS - SPIFFS.end(); - lcd_display(F("Updating WiFi"), 0, 0, 0, LCD_CLEAR_LINE); lcd_display(F(""), 0, 1, 10 * 1000, LCD_CLEAR_LINE); lcd_loop(); From 787b53f414befafbeac198424aa4935702b7a8db Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Wed, 24 Jun 2020 00:04:39 +0100 Subject: [PATCH 12/18] Latest versions of libraries --- platformio.ini | 51 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/platformio.ini b/platformio.ini index 6fead1c..54e5edf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,23 +34,32 @@ version = -DBUILD_TAG=2.8.1 monitor_speed=115200 lib_deps = PubSubClient@2.6 - ESP Async WebServer@1.1.1 - ESPAsyncTCP@1.1.3 + ESP Async WebServer@1.2.3 + ESPAsyncTCP@1.2.2 ArduinoJson@6.15.1 Micro Debug@0.0.3 ConfigJson@0.0.3 - OpenEVSE@0.0.2 - ESPAL@0.0.1 + OpenEVSE@0.0.3 + ESPAL@0.0.2 extra_scripts = scripts/extra_script.py -debug_flags = -DENABLE_DEBUG -DENABLE_PROFILE -DDEBUG_PORT=Serial1 -ota_flags = -DENABLE_OTA -DWIFI_LED=0 -build_flags = +debug_flags = -DENABLE_DEBUG -# -DENABLE_ASYNC_WIFI_SCAN +# -DENABLE_DEBUG_WEB +# -DENABLE_DEBUG_RAPI + -DENABLE_PROFILE + -DDEBUG_PORT=Serial1 +ota_flags = + -DENABLE_OTA + -DWIFI_LED=0 +src_build_flags = +# -DENABLE_ASYNC_WIFI_SCAN + +build_flags = # specify exact Arduino ESP SDK version, requires platformio 3.5+ (curently dev version) # http://docs.platformio.org/en/latest/projectconf/section_env_general.html#platform -platform = https://github.com/platformio/platform-espressif8266.git#release/v1.6.0 +#platform = https://github.com/platformio/platform-espressif8266.git#release/v1.6.0 +platform = espressif8266@2.5.2 platform_stage = https://github.com/platformio/platform-espressif8266.git#develop [env:openevse] @@ -58,7 +67,8 @@ platform = ${common.platform} board = esp12e framework = arduino lib_deps = ${common.lib_deps} -src_build_flags = ${common.version} ${common.build_flags} +build_flags = ${common.build_flags} +src_build_flags = ${common.version} ${common.src_build_flags} # Upload at faster baud: takes 20s instead of 50s. Use 'pio run -t upload -e evse_slow to use slower default baud rate' upload_speed = 921600 monitor_speed = ${common.monitor_speed} @@ -69,7 +79,8 @@ platform = ${common.platform} board = esp12e framework = arduino lib_deps = ${common.lib_deps} -src_build_flags = ${common.version} ${common.build_flags} +build_flags = ${common.build_flags} +src_build_flags = ${common.version} ${common.src_build_flags} monitor_speed = ${common.monitor_speed} extra_scripts = ${common.extra_scripts} @@ -78,9 +89,10 @@ platform = ${common.platform} board = esp12e framework = arduino lib_deps = ${common.lib_deps} -src_build_flags = ${common.version}.dev ${common.build_flags} ${common.ota_flags} ${common.debug_flags} -upload_protocol = espota -upload_port = openevse.local +build_flags = ${common.build_flags} ${common.debug_flags} +src_build_flags = ${common.version}.dev ${common.src_build_flags} ${common.ota_flags} ${common.debug_flags} +#upload_protocol = espota +#upload_port = openevse.local monitor_speed = ${common.monitor_speed} extra_scripts = ${common.extra_scripts} @@ -89,7 +101,8 @@ platform = ${common.platform} board = esp12e framework = arduino lib_deps = ${common.lib_deps} -src_build_flags = ${common.version}.dev ${common.build_flags} ${common.ota_flags} ${common.debug_flags} +build_flags = ${common.build_flags} ${common.debug_flags} +src_build_flags = ${common.version}.dev ${common.src_build_flags} ${common.ota_flags} ${common.debug_flags} # Upload at faster baud: takes 20s instead of 50s. Use 'pio run -t upload -e evse_slow to use slower default baud rate' upload_speed = 921600 monitor_speed = ${common.monitor_speed} @@ -102,8 +115,9 @@ platform = ${common.platform_stage} board = esp12e framework = arduino lib_deps = ${common.lib_deps} -build_flags = -DDEBUG_ESP_WIFI -src_build_flags = ${common.version}.stag ${common.build_flags} ${common.ota_flags} ${common.debug_flags} +build_flags = ${common.build_flags} ${common.debug_flags} +src_build_flags = ${common.version}.stag ${common.src_build_flags} ${common.ota_flags} ${common.debug_flags} +#upload_speed = 921600 upload_protocol = espota upload_port = openevse.local monitor_speed = ${common.monitor_speed} @@ -115,7 +129,8 @@ platform = ${common.platform_stage} board = esp12e framework = arduino lib_deps = https://github.com/knolleary/pubsubclient, https://github.com/me-no-dev/ESPAsyncWebServer.git, https://github.com/me-no-dev/ESPAsyncTCP.git -src_build_flags = ${common.version}.stagelib ${common.build_flags} ${common.ota_flags} ${common.debug_flags} +build_flags = ${common.build_flags} ${common.debug_flags} +src_build_flags = ${common.version}.stagelib ${common.src_build_flags} ${common.ota_flags} ${common.debug_flags} upload_protocol = espota upload_port = openevse.local monitor_speed = ${common.monitor_speed} From c6d2a741dca60cfe3a7c3f1d53e61db03467188e Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Tue, 30 Jun 2020 22:15:32 +0100 Subject: [PATCH 13/18] Default should not have https:// for ESP8266 --- src/app_config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app_config.cpp b/src/app_config.cpp index 0937f53..31c75c4 100644 --- a/src/app_config.cpp +++ b/src/app_config.cpp @@ -75,7 +75,7 @@ ConfigOpt *opts[] = new ConfigOptDefenition(esp_hostname, esp_hostname_default, "hostname", "hn"), // EMONCMS SERVER strings - new ConfigOptDefenition(emoncms_server, "https://data.openevse.com/emoncms", "emoncms_server", "es"), + new ConfigOptDefenition(emoncms_server, "data.openevse.com/emoncms", "emoncms_server", "es"), new ConfigOptDefenition(emoncms_node, esp_hostname, "emoncms_node", "en"), new ConfigOptSecret(emoncms_apikey, "", "emoncms_apikey", "ea"), new ConfigOptDefenition(emoncms_fingerprint, "", "emoncms_fingerprint", "ef"), From 3e33c315d0a454d87f9aa7b8d8dd73bba7401475 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Tue, 30 Jun 2020 22:19:45 +0100 Subject: [PATCH 14/18] Drop the -xxxx from the hostname so not to confuse existing users --- src/app_config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app_config.cpp b/src/app_config.cpp index 31c75c4..ca9e47b 100644 --- a/src/app_config.cpp +++ b/src/app_config.cpp @@ -55,7 +55,7 @@ double divert_attack_smoothing_factor; double divert_decay_smoothing_factor; uint32_t divert_min_charge_time; -String esp_hostname_default = "openevse-"+ESPAL.getShortId(); +String esp_hostname_default = "openevse"; void config_changed(String name); From ec193a61a5ad120bf5e84d321b0b3ca2bfd8be27 Mon Sep 17 00:00:00 2001 From: Jeremy Poulter Date: Tue, 30 Jun 2020 22:34:54 +0100 Subject: [PATCH 15/18] Latest GUI --- gui | 2 +- src/web_static/web_server.home.html.h | 4 +- src/web_static/web_server.home.js.h | 2 +- src/web_static/web_server.zones.json.h | 987 +++++++++++++++++++++++ src/web_static/web_server_static_files.h | 2 + 5 files changed, 993 insertions(+), 4 deletions(-) create mode 100644 src/web_static/web_server.zones.json.h diff --git a/gui b/gui index 010714a..6f35ebd 160000 --- a/gui +++ b/gui @@ -1 +1 @@ -Subproject commit 010714af0a115db91fbb1ed9270e8641edff4bca +Subproject commit 6f35ebddd48601020b616377120ae94cdef105da diff --git a/src/web_static/web_server.home.html.h b/src/web_static/web_server.home.html.h index 0e014c4..40e3b8d 100644 --- a/src/web_static/web_server.home.html.h +++ b/src/web_static/web_server.home.html.h @@ -1,5 +1,5 @@ static const char CONTENT_HOME_HTML[] PROGMEM = - " OpenEVSE

OpenEVSE

WiFi

Loading, please wait... (/)

WiFi Setup

Mode:

IP Address:

Successful packets:
of

OpenEVSE

RAPI packets:
of

Network RSSI dBm

IP Address:

Successful packets:
of

OpenEVSE

RAPI packets:
of

Connect to network:

Scanning...

SSID:

Passkey:

Connecting to WIFI Network...

Administration

Username:
15 characters max

Password:


15 characters max
Web interface HTTP authentication.

WiFi Firmware

ESP8266

Version:

Error:

Updating...
Firmware update completed ok

Advanced Settings

Hostname:
31 characters max

NTP Server:

Developer Mode

Enabled:

  Energy Monitoring

Emoncms Server*:
0 }\">

WiFi Setup

Mode:

IP Address:

Successful packets:
of

OpenEVSE

RAPI packets:
of

Network RSSI dBm

IP Address:

Successful packets:
of

OpenEVSE

RAPI packets:
of

Connect to network:

Scanning...

SSID:

Passkey:

Connecting to WIFI Network...

Administration

Username:
15 characters max

Password:


15 characters max
Web interface HTTP authentication.

WiFi Firmware

Version:

Error:

Updating...
Firmware update completed ok

Advanced Settings

Hostname:
31 characters max

NTP Server:

Developer Mode

Enabled:

  Energy Monitoring

Emoncms Server*:
0 }\">

Time zone:

Set time from

Service Level:

Set time from

Service Level: