From d30b0ce505e8f7913cf829e1ff3ad981dc0d40f7 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 23 Jul 2020 12:02:58 +0200 Subject: [PATCH 01/66] supress compile warnings --- lib/OneWire/OneWire.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/OneWire/OneWire.cpp b/lib/OneWire/OneWire.cpp index 7a4000e64..5828e804d 100644 --- a/lib/OneWire/OneWire.cpp +++ b/lib/OneWire/OneWire.cpp @@ -144,7 +144,7 @@ sample code bearing this copyright. #include "OneWire_direct_gpio.h" #pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-variable" void OneWire::begin(uint8_t pin) { pinMode(pin, INPUT); @@ -155,7 +155,6 @@ void OneWire::begin(uint8_t pin) { #endif } - // Perform the onewire reset function. We will wait up to 250uS for // the bus to come high, if it doesn't then it is broken or shorted // and we return a 0; From 5cd1c4eb7d2af71fe613bd4f18d356f18c30708c Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 23 Jul 2020 12:03:25 +0200 Subject: [PATCH 02/66] loop() doesn't need to be static anymore --- src/console.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/console.h b/src/console.h index 095bed9ba..5b335503c 100644 --- a/src/console.h +++ b/src/console.h @@ -191,8 +191,8 @@ class EMSESPStreamConsole : public uuid::console::StreamConsole, public EMSESPSh class Console { public: - static void loop(); - void start(); + void loop(); + void start(); uuid::log::Level log_level(); From a09dd3b735869cd64757c52b53204f6d81115fb0 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 23 Jul 2020 12:03:50 +0200 Subject: [PATCH 03/66] fix warning in console when setting wifi params --- src/system.cpp | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index b21db535f..284c8b939 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -373,9 +373,7 @@ int8_t System::wifi_quality() { } void System::show_system(uuid::console::Shell & shell) { - shell.print(F("Uptime: ")); - shell.print(uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3)); - shell.println(); + shell.printfln(F("Uptime: %s"), uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3).c_str()); #if defined(ESP8266) shell.printfln(F("Chip ID: 0x%08x"), ESP.getChipId()); @@ -389,7 +387,7 @@ void System::show_system(uuid::console::Shell & shell) { shell.printfln(F("Sketch size: %u bytes (%u bytes free)"), ESP.getSketchSize(), ESP.getFreeSketchSpace()); shell.printfln(F("Reset reason: %s"), ESP.getResetReason().c_str()); shell.printfln(F("Reset info: %s"), ESP.getResetInfo().c_str()); - + shell.println(); shell.printfln(F("Free heap: %lu bytes"), (unsigned long)ESP.getFreeHeap()); shell.printfln(F("Free mem: %d %%"), free_mem()); shell.printfln(F("Maximum free block size: %lu bytes"), (unsigned long)ESP.getMaxFreeBlockSize()); @@ -408,21 +406,19 @@ void System::show_system(uuid::console::Shell & shell) { #ifndef EMSESP_STANDALONE switch (WiFi.status()) { case WL_IDLE_STATUS: - shell.printfln(F("WiFi: idle")); + shell.printfln(F("WiFi: Idle")); break; case WL_NO_SSID_AVAIL: - shell.printfln(F("WiFi: network not found")); + shell.printfln(F("WiFi: Network not found")); break; case WL_SCAN_COMPLETED: - shell.printfln(F("WiFi: network scan complete")); + shell.printfln(F("WiFi: Network scan complete")); break; case WL_CONNECTED: { - shell.printfln(F("WiFi: connected")); - shell.println(); - + shell.printfln(F("WiFi: Connected")); shell.printfln(F("SSID: %s"), WiFi.SSID().c_str()); shell.printfln(F("BSSID: %s"), WiFi.BSSIDstr().c_str()); shell.printfln(F("RSSI: %d dBm (%d %%)"), WiFi.RSSI(), wifi_quality()); @@ -432,27 +428,26 @@ void System::show_system(uuid::console::Shell & shell) { #elif defined(ESP32) shell.printfln(F("Hostname: %s"), WiFi.getHostname()); #endif - shell.println(); shell.printfln(F("IPv4 address: %s/%s"), uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str()); shell.printfln(F("IPv4 gateway: %s"), uuid::printable_to_string(WiFi.gatewayIP()).c_str()); shell.printfln(F("IPv4 nameserver: %s"), uuid::printable_to_string(WiFi.dnsIP()).c_str()); } break; case WL_CONNECT_FAILED: - shell.printfln(F("WiFi: connection failed")); + shell.printfln(F("WiFi: Connection failed")); break; case WL_CONNECTION_LOST: - shell.printfln(F("WiFi: connection lost")); + shell.printfln(F("WiFi: Connection lost")); break; case WL_DISCONNECTED: - shell.printfln(F("WiFi: disconnected")); + shell.printfln(F("WiFi: Disconnected")); break; case WL_NO_SHIELD: default: - shell.printfln(F("WiFi: unknown")); + shell.printfln(F("WiFi: Unknown")); break; } @@ -535,8 +530,9 @@ void System::console_commands(Shell & shell, unsigned int context) { flash_string_vector{F_(set), F_(wifi), F_(hostname)}, flash_string_vector{F_(name_mandatory)}, [](Shell & shell __attribute__((unused)), const std::vector & arguments) { - shell.println("Note, connection will be reset..."); - Console::loop(); + shell.println("The wifi connection will be reset..."); + Shell::loop_all(); + delay(1000); // wait a second EMSESP::esp8266React.getWiFiSettingsService()->update( [&](WiFiSettings & wifiSettings) { wifiSettings.hostname = arguments.front().c_str(); @@ -550,8 +546,9 @@ void System::console_commands(Shell & shell, unsigned int context) { flash_string_vector{F_(set), F_(wifi), F_(ssid)}, flash_string_vector{F_(name_mandatory)}, [](Shell & shell, const std::vector & arguments) { - shell.println("Note, connection will be reset..."); - Console::loop(); + shell.println("The wifi connection will be reset..."); + Shell::loop_all(); + delay(1000); // wait a second EMSESP::esp8266React.getWiFiSettingsService()->update( [&](WiFiSettings & wifiSettings) { wifiSettings.ssid = arguments.front().c_str(); From b0fa45ecca0648f40eaf7775b5e25f12448bfb98 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 25 Jul 2020 10:59:48 +0200 Subject: [PATCH 04/66] removed bogus files --- media/EMS-ESP_logo.png:Zone.Identifier | 3 --- media/EMS-ESP_logo_dark.png:Zone.Identifier | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 media/EMS-ESP_logo.png:Zone.Identifier delete mode 100644 media/EMS-ESP_logo_dark.png:Zone.Identifier diff --git a/media/EMS-ESP_logo.png:Zone.Identifier b/media/EMS-ESP_logo.png:Zone.Identifier deleted file mode 100644 index 2dac2bc63..000000000 --- a/media/EMS-ESP_logo.png:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -LastWriterPackageFamilyName=Microsoft.MSPaint_8wekyb3d8bbwe -ZoneId=3 diff --git a/media/EMS-ESP_logo_dark.png:Zone.Identifier b/media/EMS-ESP_logo_dark.png:Zone.Identifier deleted file mode 100644 index 2dac2bc63..000000000 --- a/media/EMS-ESP_logo_dark.png:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -LastWriterPackageFamilyName=Microsoft.MSPaint_8wekyb3d8bbwe -ZoneId=3 From 7a88601ccdad884721fdc9e6935724d02e2ebf1f Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 25 Jul 2020 11:00:27 +0200 Subject: [PATCH 05/66] fix hostname when using static ip - #432 --- README.md | 2 +- lib/framework/WiFiSettingsService.cpp | 131 +++++++++++++------------- 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 946a83629..676367c77 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ The Web is based off Rick's awesome [esp8266-react](https://github.com/rjwats/es * `MqttStatus.cpp` added root["mqtt_fails"] * `SecuritySettingsService.cpp` added version to the JWT payload * `SecuritySettingsService.h` #include "../../src/version.h" - * `WiFiSettingsService.cpp` added WiFi.setOutputPower(20.0f) - removed + * `WiFiSettingsService.cpp` added WiFi.setOutputPower(20.0f), moved setHostname * `OTASettingsService.h` added #include "../../src/system.h" * `OTASettingsService.cpp` added call to emsesp::System::upload_status(true) * `features.ini`: -D FT_NTP=0 diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index 8f03a3e2f..2c9c9ad5e 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -1,27 +1,25 @@ #include -WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(WiFiSettings::read, WiFiSettings::update, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager), - _fsPersistence(WiFiSettings::read, WiFiSettings::update, this, fs, WIFI_SETTINGS_FILE), - _lastConnectionAttempt(0) { - // We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default. - // If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future. - if (WiFi.getMode() != WIFI_OFF) { - WiFi.mode(WIFI_OFF); - } +WiFiSettingsService::WiFiSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager) + : _httpEndpoint(WiFiSettings::read, WiFiSettings::update, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager) + , _fsPersistence(WiFiSettings::read, WiFiSettings::update, this, fs, WIFI_SETTINGS_FILE) + , _lastConnectionAttempt(0) { + // We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default. + // If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future. + if (WiFi.getMode() != WIFI_OFF) { + WiFi.mode(WIFI_OFF); + } - // Disable WiFi config persistance and auto reconnect - WiFi.persistent(false); - WiFi.setAutoReconnect(false); + // Disable WiFi config persistance and auto reconnect + WiFi.persistent(false); + WiFi.setAutoReconnect(false); #ifdef ESP32 - // Init the wifi driver on ESP32 - WiFi.mode(WIFI_MODE_MAX); - WiFi.mode(WIFI_MODE_NULL); - WiFi.onEvent( - std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), - WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); - WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeStop, this, std::placeholders::_1, std::placeholders::_2), - WiFiEvent_t::SYSTEM_EVENT_STA_STOP); + // Init the wifi driver on ESP32 + WiFi.mode(WIFI_MODE_MAX); + WiFi.mode(WIFI_MODE_NULL); + WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), + WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); + WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeStop, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_STOP); #elif defined(ESP8266) // proddy added @@ -31,78 +29,81 @@ WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, Securit // high tx power causing weird behavior, slightly lowering from 20.5 to 20.0 may help stability // WiFi.setOutputPower(20.0f); // in dBm - _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected( - std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); + _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); #endif - addUpdateHandler([&](const String& originId) { reconfigureWiFiConnection(); }, false); + addUpdateHandler([&](const String & originId) { reconfigureWiFiConnection(); }, false); } void WiFiSettingsService::begin() { - _fsPersistence.readFromFS(); - reconfigureWiFiConnection(); + _fsPersistence.readFromFS(); + reconfigureWiFiConnection(); } void WiFiSettingsService::reconfigureWiFiConnection() { - // reset last connection attempt to force loop to reconnect immediately - _lastConnectionAttempt = 0; + // reset last connection attempt to force loop to reconnect immediately + _lastConnectionAttempt = 0; // disconnect and de-configure wifi #ifdef ESP32 - if (WiFi.disconnect(true)) { - _stopping = true; - } + if (WiFi.disconnect(true)) { + _stopping = true; + } #elif defined(ESP8266) - WiFi.disconnect(true); + WiFi.disconnect(true); #endif } void WiFiSettingsService::loop() { - unsigned long currentMillis = millis(); - if (!_lastConnectionAttempt || (unsigned long)(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY) { - _lastConnectionAttempt = currentMillis; - manageSTA(); - } + unsigned long currentMillis = millis(); + if (!_lastConnectionAttempt || (unsigned long)(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY) { + _lastConnectionAttempt = currentMillis; + manageSTA(); + } } void WiFiSettingsService::manageSTA() { - // Abort if already connected, or if we have no SSID - if (WiFi.isConnected() || _state.ssid.length() == 0) { - return; - } - // Connect or reconnect as required - if ((WiFi.getMode() & WIFI_STA) == 0) { - // Serial.println(F("Connecting to WiFi.")); - if (_state.staticIPConfig) { - // configure for static IP - WiFi.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); - } else { - // configure for DHCP -#ifdef ESP32 - WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); - WiFi.setHostname(_state.hostname.c_str()); -#elif defined(ESP8266) - WiFi.config(INADDR_ANY, INADDR_ANY, INADDR_ANY); - WiFi.hostname(_state.hostname); -#endif + // Abort if already connected, or if we have no SSID + if (WiFi.isConnected() || _state.ssid.length() == 0) { + return; + } + // Connect or reconnect as required + if ((WiFi.getMode() & WIFI_STA) == 0) { + // Serial.println(F("Connecting to WiFi.")); + if (_state.staticIPConfig) { + // configure for static IP + WiFi.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); + } else { + // configure for DHCP +#ifdef ESP32 + WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); +#elif defined(ESP8266) + WiFi.config(INADDR_ANY, INADDR_ANY, INADDR_ANY); +#endif + } + // set hostname +#ifdef ESP32 + WiFi.setHostname(_state.hostname.c_str()); +#elif defined(ESP8266) + WiFi.hostname(_state.hostname); +#endif + // attempt to connect to the network + WiFi.begin(_state.ssid.c_str(), _state.password.c_str()); } - // attempt to connect to the network - WiFi.begin(_state.ssid.c_str(), _state.password.c_str()); - } } #ifdef ESP32 void WiFiSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { - WiFi.disconnect(true); + WiFi.disconnect(true); } void WiFiSettingsService::onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info) { - if (_stopping) { - _lastConnectionAttempt = 0; - _stopping = false; - } + if (_stopping) { + _lastConnectionAttempt = 0; + _stopping = false; + } } #elif defined(ESP8266) -void WiFiSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { - WiFi.disconnect(true); +void WiFiSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected & event) { + WiFi.disconnect(true); } #endif From 61a711c8f6b85667a80fbe38ee6de3f82730b874 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:24:45 +0200 Subject: [PATCH 06/66] added show users command - #435 --- README.md | 1 + src/system.cpp | 32 ++++++++++++++++++++++++-------- src/system.h | 5 +++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 946a83629..72da0e0d8 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ system set show show mqtt + show users passwd restart set wifi hostname diff --git a/src/system.cpp b/src/system.cpp index 284c8b939..0b5bedb27 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -26,6 +26,7 @@ MAKE_PSTR_WORD(hostname) MAKE_PSTR_WORD(wifi) MAKE_PSTR_WORD(ssid) MAKE_PSTR_WORD(heartbeat) +MAKE_PSTR_WORD(users) MAKE_PSTR(host_fmt, "Host = %s") MAKE_PSTR(hostname_fmt, "WiFi Hostname = %s") @@ -221,17 +222,15 @@ void System::start() { } // fetch settings + std::string hostname; EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode_ = settings.tx_mode; }); EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { system_heartbeat_ = settings.system_heartbeat; }); + EMSESP::esp8266React.getWiFiSettingsService()->read( + [&](WiFiSettings & wifiSettings) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), wifiSettings.hostname, EMSESP_APP_VERSION); }); + syslog_.log_level((uuid::log::Level)syslog_level_); syslog_init(); // init SysLog -#if defined(ESP32) - LOG_INFO(F("System booted (EMS-ESP version %s ESP32)"), EMSESP_APP_VERSION); -#else - LOG_INFO(F("System booted (EMS-ESP version %s)"), EMSESP_APP_VERSION); -#endif - if (LED_GPIO) { pinMode(LED_GPIO, OUTPUT); // LED pin, 0 means disabled } @@ -372,6 +371,19 @@ int8_t System::wifi_quality() { return 2 * (dBm + 100); } +// print users to console +void System::show_users(uuid::console::Shell & shell) { + shell.printfln(F("Users:")); + + EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { + for (User user : securitySettings.users) { + shell.printfln(F(" username: %s password: %s is_admin: %s"), user.username, user.password, user.admin ? "yes" : "no"); + } + }); + + shell.println(); +} + void System::show_system(uuid::console::Shell & shell) { shell.printfln(F("Uptime: %s"), uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3).c_str()); @@ -609,6 +621,11 @@ void System::console_commands(Shell & shell, unsigned int context) { flash_string_vector{F_(show), F_(mqtt)}, [](Shell & shell, const std::vector & arguments __attribute__((unused))) { Mqtt::show_mqtt(shell); }); + EMSESPShell::commands->add_command(ShellContext::SYSTEM, + CommandFlags::ADMIN, + flash_string_vector{F_(show), F_(users)}, + [](Shell & shell, const std::vector & arguments __attribute__((unused))) { System::show_users(shell); }); + // enter the context Console::enter_custom_context(shell, context); @@ -618,8 +635,7 @@ void System::console_commands(Shell & shell, unsigned int context) { void System::check_upgrade() { // check for v1.9. It uses SPIFFS and only on the ESP8266 #if defined(ESP8266) - - Serial.begin(115200); // TODO remove + Serial.begin(115200); // TODO remove, just for debugging #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" diff --git a/src/system.h b/src/system.h index 3b3cd0d52..3aa20de2e 100644 --- a/src/system.h +++ b/src/system.h @@ -50,8 +50,8 @@ class System { static void mqtt_commands(const char * message); static uint8_t free_mem(); - static void upload_status(bool in_progress); - static bool upload_status(); + static void upload_status(bool in_progress); + static bool upload_status(); void syslog_init(); @@ -97,6 +97,7 @@ class System { void system_check(); static void show_system(uuid::console::Shell & shell); + static void show_users(uuid::console::Shell & shell); static int8_t wifi_quality(); bool system_healthy_ = false; From bb8e1676f8a88685473379fe17cd231101b2191b Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:27:13 +0200 Subject: [PATCH 07/66] cleanup --- src/EMSESPScanDevicesService.cpp | 20 -------------------- src/EMSESPScanDevicesService.h | 21 --------------------- src/test/test.cpp | 3 +++ 3 files changed, 3 insertions(+), 41 deletions(-) delete mode 100644 src/EMSESPScanDevicesService.cpp delete mode 100644 src/EMSESPScanDevicesService.h diff --git a/src/EMSESPScanDevicesService.cpp b/src/EMSESPScanDevicesService.cpp deleted file mode 100644 index 0abb917be..000000000 --- a/src/EMSESPScanDevicesService.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include "emsesp.h" - -namespace emsesp { - -EMSESPScanDevicesService::EMSESPScanDevicesService(AsyncWebServer * server, SecurityManager * securityManager) { - server->on(SCAN_DEVICES_SERVICE_PATH, - HTTP_POST, - securityManager->wrapRequest(std::bind(&EMSESPScanDevicesService::scan_devices, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN)); -} - -void EMSESPScanDevicesService::scan_devices(AsyncWebServerRequest * request) { - request->onDisconnect([]() { - EMSESP::send_read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER); - }); - request->send(200); -} - -} // namespace emsesp diff --git a/src/EMSESPScanDevicesService.h b/src/EMSESPScanDevicesService.h deleted file mode 100644 index 0b3a873e4..000000000 --- a/src/EMSESPScanDevicesService.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef EMSESPScanDevicesService_h -#define EMSESPScanDevicesService_h - -#include -#include - -#define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices" - -namespace emsesp { - -class EMSESPScanDevicesService { - public: - EMSESPScanDevicesService(AsyncWebServer * server, SecurityManager * securityManager); - - private: - void scan_devices(AsyncWebServerRequest * request); -}; - -} // namespace emsesp - -#endif diff --git a/src/test/test.cpp b/src/test/test.cpp index 03a1de80c..f8f77ac74 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +#if defined(EMSESP_STANADLONE) #include "test.h" @@ -692,3 +693,5 @@ void Test::dummy_mqtt_commands(const char * message) { #pragma GCC diagnostic pop } // namespace emsesp + +#endif From 82d8210754aabf6294fe46411d490fe32943055a Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:27:23 +0200 Subject: [PATCH 08/66] b8 --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index 65f946843..ebc93e297 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "2.0.0b7" +#define EMSESP_APP_VERSION "2.0.0b8" From 580f3ea45c86f4a24aab3ac5d3d44dd7c46843ee Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:27:37 +0200 Subject: [PATCH 09/66] optimizing mqtt --- src/mqtt.cpp | 152 +++++++++++++++++++++++++++++---------------------- src/mqtt.h | 35 ++++++------ 2 files changed, 105 insertions(+), 82 deletions(-) diff --git a/src/mqtt.cpp b/src/mqtt.cpp index a616ec4c8..ce596dc66 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -35,7 +35,7 @@ std::string Mqtt::hostname_; uint8_t Mqtt::mqtt_qos_; uint16_t Mqtt::publish_time_; -std::vector Mqtt::mqtt_functions_; +std::vector Mqtt::mqtt_subfunctions_; uint16_t Mqtt::mqtt_publish_fails_ = 0; size_t Mqtt::maximum_mqtt_messages_ = Mqtt::MAX_MQTT_MESSAGES; uint16_t Mqtt::mqtt_message_id_ = 0; @@ -51,62 +51,56 @@ Mqtt::QueuedMqttMessage::QueuedMqttMessage(uint16_t id, std::shared_ptrtopic.c_str()) == 0)) { exists = true; } } } if (!exists) { - mqtt_functions_.emplace_back(device_id, std::move(full_topic), cb); // register a call back function for a specific telegram type + mqtt_subfunctions_.emplace_back(device_id, std::move(message->topic), cb); // register a call back function for a specific telegram type } - - queue_subscribe_message(topic); // add subscription to queue } // subscribe to an MQTT topic, and store the associated callback function. For generic functions not tied to a specific device -void Mqtt::subscribe(const std::string & topic, mqtt_function_p cb) { +void Mqtt::subscribe(const std::string & topic, mqtt_subfunction_p cb) { subscribe(0, topic, cb); // no device_id needed, if generic to EMS-ESP } // resubscribe to all MQTT topics again void Mqtt::resubscribe() { - if (mqtt_functions_.empty()) { + if (mqtt_subfunctions_.empty()) { return; } - for (const auto & mqtt_function : mqtt_functions_) { - queue_subscribe_message(mqtt_function.topic_); + for (const auto & mqtt_subfunction : mqtt_subfunctions_) { + queue_message(Operation::SUBSCRIBE, mqtt_subfunction.topic_, "", false, true); // no payload, no topic prefixing } } @@ -146,8 +140,8 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) { // show subscriptions shell.printfln(F("MQTT subscriptions:")); - for (const auto & mqtt_function : mqtt_functions_) { - shell.printfln(F(" %s"), mqtt_function.topic_.c_str()); + for (const auto & mqtt_subfunction : mqtt_subfunctions_) { + shell.printfln(F(" %s"), mqtt_subfunction.topic_.c_str()); } shell.println(); @@ -216,9 +210,9 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) { // see if we have this topic in our subscription list, then call its callback handler // note: this will pick the first topic that matches, so for multiple devices of the same type it's gonna fail. Not sure if this is going to be an issue? - for (const auto & mf : mqtt_functions_) { + for (const auto & mf : mqtt_subfunctions_) { if (strcmp(topic, mf.topic_.c_str()) == 0) { - (mf.mqtt_function_)(message); + (mf.mqtt_subfunction_)(message); return; } } @@ -229,15 +223,17 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) { // print all the topics related to a specific device_id void Mqtt::show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_id) { - if (std::count_if(mqtt_functions_.cbegin(), mqtt_functions_.cend(), [=](MQTTFunction const & mqtt_function) { return device_id == mqtt_function.device_id_; }) + if (std::count_if(mqtt_subfunctions_.cbegin(), + mqtt_subfunctions_.cend(), + [=](MQTTSubFunction const & mqtt_subfunction) { return device_id == mqtt_subfunction.device_id_; }) == 0) { return; } shell.print(F(" Subscribed MQTT topics: ")); - for (const auto & mqtt_function : mqtt_functions_) { - if (mqtt_function.device_id_ == device_id) { - shell.printf(F("%s "), mqtt_function.topic_.c_str()); + for (const auto & mqtt_subfunction : mqtt_subfunctions_) { + if (mqtt_subfunction.device_id_ == device_id) { + shell.printf(F("%s "), mqtt_subfunction.topic_.c_str()); } } shell.println(); @@ -285,7 +281,6 @@ char * Mqtt::make_topic(char * result, const std::string & topic) { } void Mqtt::start() { - mqttClient_ = EMSESP::esp8266React.getMqttClient(); // get the hostname, which we'll use to prefix to all topics @@ -298,15 +293,18 @@ void Mqtt::start() { }); mqttClient_->onConnect([this](bool sessionPresent) { on_connect(); }); - mqttClient_->setWill(make_topic(will_topic_, "status"), 1, true, "offline"); // with qos 1, retain true + + // create will_topic with the hostname prefixed. It has to be static because asyncmqttclient destroys the reference + static char will_topic[MQTT_TOPIC_MAX_SIZE]; + strlcpy(will_topic, hostname_.c_str(), MQTT_TOPIC_MAX_SIZE); + strlcat(will_topic, "/", MQTT_TOPIC_MAX_SIZE); + strlcat(will_topic, "status", MQTT_TOPIC_MAX_SIZE); + mqttClient_->setWill(will_topic, 1, true, "offline"); // with qos 1, retain true + mqttClient_->onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { on_message(topic, payload, len); mqttClient_->onPublish([this](uint16_t packetId) { on_publish(packetId); }); }); - - // add the system MQTT subscriptions - Mqtt::subscribe("cmd", System::mqtt_commands); - // Mqtt::subscribe("cmd", std::bind(&System::mqtt_commands, this, std::placeholders::_1)); } void Mqtt::set_publish_time(uint16_t publish_time) { @@ -334,47 +332,50 @@ void Mqtt::on_connect() { resubscribe(); // in case this is a reconnect, re-subscribe again to all MQTT topics + // add the system MQTT subscriptions, only if its a fresh start with no previous subscriptions + if (mqtt_subfunctions_.empty()) { + Mqtt::subscribe("cmd", System::mqtt_commands); + } + LOG_INFO(F("MQTT connected")); } -// add MQTT message to queue, payload is a string -void Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, const bool retain) { - // can't have bogus topics, but empty payloads are ok +// add sub or pub task to the queue. When the message is created, the topic will have +// automatically the hostname prefixed. +std::shared_ptr +Mqtt::queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain, bool no_prefix) { if (topic.empty()) { - return; + return nullptr; } - // prefix the hostname to the topic - char full_topic[MQTT_TOPIC_MAX_SIZE]; - make_topic(full_topic, topic); - - auto message = std::make_shared(Operation::PUBLISH, full_topic, payload, retain); + // take the topic and prefix the hostname, unless its for HA + std::shared_ptr message; + if ((strncmp(topic.c_str(), "homeassistant/", 13) == 0) || no_prefix) { + // leave topic as it is + message = std::make_shared(operation, topic, payload, retain); + } else { + // prefix the hostname + std::string full_topic = Mqtt::hostname_ + "/" + topic; + message = std::make_shared(operation, full_topic, payload, retain); + } // if the queue is full, make room but removing the last one if (mqtt_messages_.size() >= maximum_mqtt_messages_) { mqtt_messages_.pop_front(); } - mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message)); + + return mqtt_messages_.back().content_; // this is because the message has been moved +} + +// add MQTT message to queue, payload is a string +std::shared_ptr Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, const bool retain) { + return queue_message(Operation::PUBLISH, topic, payload, retain); } // add MQTT subscribe message to queue -void Mqtt::queue_subscribe_message(const std::string & topic) { - if (topic.empty()) { - return; - } - - auto message = std::make_shared(Operation::SUBSCRIBE, topic, "", false); -#ifdef DEBUG - LOG_DEBUG(F("Adding a subscription for %s"), topic.c_str()); -#endif - - // if the queue is full, make room but removing the last one - if (mqtt_messages_.size() >= maximum_mqtt_messages_) { - mqtt_messages_.pop_front(); - } - - mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message)); +std::shared_ptr Mqtt::queue_subscribe_message(const std::string & topic) { + return queue_message(Operation::SUBSCRIBE, topic, "", false); // no payload } // MQTT Publish, using a specific retain flag @@ -383,9 +384,8 @@ void Mqtt::publish(const std::string & topic, const std::string & payload, bool } void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool retain) { - // convert json to string std::string payload_text; - serializeJson(payload, payload_text); + serializeJson(payload, payload_text); // convert json to string queue_publish_message(topic, payload_text, retain); } @@ -413,6 +413,26 @@ void Mqtt::process_queue() { return; } + // show queue - Debug only + /* + Serial.printf("MQTT queue:\n\r"); + for (const auto & message : mqtt_messages_) { + auto content = message.content_; + if (content->operation == Operation::PUBLISH) { + // Publish messages + Serial.printf(" [%02d] (Pub) topic=%s payload=%s (pid %d, retry #%d)\n\r", + message.id_, + content->topic.c_str(), + content->payload.c_str(), + message.packet_id_, + message.retry_count_); + } else { + // Subscribe messages + Serial.printf(" [%02d] (Sub) topic=%s\n\r", message.id_, content->topic.c_str()); + } + } + */ + // fetch first from queue and create the full topic name auto mqtt_message = mqtt_messages_.front(); auto message = mqtt_message.content_; diff --git a/src/mqtt.h b/src/mqtt.h index 992286d12..c9d233c76 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -43,11 +43,11 @@ using uuid::console::Shell; namespace emsesp { -using mqtt_function_p = std::function; +using mqtt_subfunction_p = std::function; using namespace std::placeholders; // for `_1` struct MqttMessage { - MqttMessage(uint8_t operation, const std::string & topic, const std::string & payload, bool retain); + MqttMessage(const uint8_t operation, const std::string & topic, const std::string & payload, bool retain); ~MqttMessage() = default; const uint8_t operation; @@ -68,8 +68,8 @@ class Mqtt { static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 100; - static void subscribe(const uint8_t device_id, const std::string & topic, mqtt_function_p cb); - static void subscribe(const std::string & topic, mqtt_function_p cb); + static void subscribe(const uint8_t device_id, const std::string & topic, mqtt_subfunction_p cb); + static void subscribe(const std::string & topic, mqtt_subfunction_p cb); static void resubscribe(); static void publish(const std::string & topic, const std::string & payload, bool retain = false); @@ -100,6 +100,8 @@ class Mqtt { mqtt_publish_fails_ = 0; } + static std::string hostname_; + private: static uuid::log::Logger logger_; @@ -125,8 +127,9 @@ class Mqtt { static constexpr uint32_t MQTT_PUBLISH_WAIT = 200; // delay between sending publishes, to account for large payloads static constexpr uint8_t MQTT_PUBLISH_MAX_RETRY = 3; // max retries for giving up on publishing - static void queue_publish_message(const std::string & topic, const std::string & payload, const bool retain); - static void queue_subscribe_message(const std::string & topic); + static std::shared_ptr queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain, bool no_prefix = false); + static std::shared_ptr queue_publish_message(const std::string & topic, const std::string & payload, const bool retain); + static std::shared_ptr queue_subscribe_message(const std::string & topic); void on_publish(uint16_t packetId); void on_message(char * topic, char * payload, size_t len); @@ -136,25 +139,25 @@ class Mqtt { static uint16_t mqtt_publish_fails_; - class MQTTFunction { + // function handlers for MQTT subscriptions + class MQTTSubFunction { public: - MQTTFunction(uint8_t device_id, const std::string && topic, mqtt_function_p mqtt_function); - ~MQTTFunction() = default; + MQTTSubFunction(const uint8_t device_id, const std::string && topic, mqtt_subfunction_p mqtt_subfunction); + ~MQTTSubFunction() = default; - uint8_t device_id_; // which device ID owns this - std::string topic_; - mqtt_function_p mqtt_function_; + const uint8_t device_id_; // which device ID owns this + const std::string topic_; + mqtt_subfunction_p mqtt_subfunction_; }; - static std::vector mqtt_functions_; // list of mqtt subscribe callbacks for all devices + static std::vector mqtt_subfunctions_; // list of mqtt subscribe callbacks for all devices uint32_t last_mqtt_poll_ = 0; uint32_t last_publish_ = 0; // settings, copied over - static std::string hostname_; - static uint8_t mqtt_qos_; - static uint16_t publish_time_; + static uint8_t mqtt_qos_; + static uint16_t publish_time_; }; } // namespace emsesp From 87f1858d5e4e5a3f3efae296c2490525dd2dcfd4 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:27:52 +0200 Subject: [PATCH 10/66] optimizing mqtt --- src/emsdevice.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emsdevice.h b/src/emsdevice.h index 8b691bcee..5030e1108 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -119,7 +119,7 @@ class EMSdevice { void read_command(const uint16_t type_id); - void register_mqtt_topic(const std::string & topic, mqtt_function_p f); + void register_mqtt_topic(const std::string & topic, mqtt_subfunction_p f); // virtual functions overrules by derived classes virtual void show_values(uuid::console::Shell & shell) = 0; From b2b6413149ce3d394a155630cfcf70eec2db3fbd Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:28:23 +0200 Subject: [PATCH 11/66] optimizing mqtt --- src/emsdevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 72fe9da06..16341f8ad 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -198,7 +198,7 @@ void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) { Mqtt::show_topic_handlers(shell, this->device_id_); } -void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_function_p f) { +void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_subfunction_p f) { LOG_DEBUG(F("Registering MQTT topic %s for device ID %02X"), topic.c_str(), this->device_id_); Mqtt::subscribe(this->device_id_, topic, f); } From 760cef90583c513ede788840291ba5fee6d9cb40 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:30:12 +0200 Subject: [PATCH 12/66] remove bogus logging --- src/EMSESPStatusService.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/EMSESPStatusService.cpp b/src/EMSESPStatusService.cpp index ba3745a01..0aaaca257 100644 --- a/src/EMSESPStatusService.cpp +++ b/src/EMSESPStatusService.cpp @@ -14,36 +14,28 @@ EMSESPStatusService::EMSESPStatusService(AsyncWebServer * server, SecurityManage // trigger on wifi connects #ifdef ESP32 - WiFi.onEvent(onStationModeConnected, WiFiEvent_t::SYSTEM_EVENT_STA_CONNECTED); WiFi.onEvent(onStationModeDisconnected, WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); WiFi.onEvent(onStationModeGotIP, WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); #elif defined(ESP8266) - _onStationModeConnectedHandler = WiFi.onStationModeConnected(onStationModeConnected); _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(onStationModeDisconnected); _onStationModeGotIPHandler = WiFi.onStationModeGotIP(onStationModeGotIP); #endif } #ifdef ESP32 -void EMSESPStatusService::onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info) { - EMSESP::logger().debug(F("Wifi Connected")); -} void EMSESPStatusService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { EMSESP::logger().debug(F("WiFi Disconnected. Reason code=%d"), info.disconnected.reason); } void EMSESPStatusService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { - EMSESP::logger().debug(F("WiFi connected with IP=%s, hostname=%s"), WiFi.localIP().toString().c_str(), WiFi.getHostname()); + EMSESP::logger().debug(F("WiFi Connected with IP=%s, hostname=%s"), WiFi.localIP().toString().c_str(), WiFi.getHostname()); EMSESP::system_.send_heartbeat(); // send out heartbeat MQTT as soon as we have a connection } #elif defined(ESP8266) -void EMSESPStatusService::onStationModeConnected(const WiFiEventStationModeConnected & event) { - EMSESP::logger().debug(F("Wifi connected with SSID %s"), event.ssid.c_str()); -} void EMSESPStatusService::onStationModeDisconnected(const WiFiEventStationModeDisconnected & event) { EMSESP::logger().debug(F("WiFi Disconnected. Reason code=%d"), event.reason); } void EMSESPStatusService::onStationModeGotIP(const WiFiEventStationModeGotIP & event) { - EMSESP::logger().debug(F("WiFi connected with IP=%s, hostname=%s"), event.ip.toString().c_str(), WiFi.hostname().c_str()); + EMSESP::logger().debug(F("WiFi Connected with IP=%s, hostname=%s"), event.ip.toString().c_str(), WiFi.hostname().c_str()); EMSESP::system_.send_heartbeat(); // send out heartbeat MQTT as soon as we have a connection } #endif From 0b0ab3121cde701de9a6c14040b5692af11dc97a Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:30:44 +0200 Subject: [PATCH 13/66] doing all web dev on ESP32 as standard now, just quicker --- interface/.env.development | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/.env.development b/interface/.env.development index 6fb8acf00..acbd05bab 100644 --- a/interface/.env.development +++ b/interface/.env.development @@ -2,9 +2,9 @@ # Remember to also enable CORS in platformio.ini before uploading the code to the device. # ESP32 dev -#REACT_APP_HTTP_ROOT=http://10.10.10.194 -#REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.194 +REACT_APP_HTTP_ROOT=http://10.10.10.194 +REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.194 # ESP8266 dev -REACT_APP_HTTP_ROOT=http://10.10.10.140 -REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.140 \ No newline at end of file +#REACT_APP_HTTP_ROOT=http://10.10.10.140 +#REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.140 \ No newline at end of file From 39e5fb1a68d10e563dc51bc4f1058242f545d0ba Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 25 Jul 2020 18:47:05 +0200 Subject: [PATCH 14/66] fix build error on esp8266 --- src/system.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index 0b5bedb27..efdafe763 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -226,7 +226,7 @@ void System::start() { EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode_ = settings.tx_mode; }); EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { system_heartbeat_ = settings.system_heartbeat; }); EMSESP::esp8266React.getWiFiSettingsService()->read( - [&](WiFiSettings & wifiSettings) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), wifiSettings.hostname, EMSESP_APP_VERSION); }); + [&](WiFiSettings & wifiSettings) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), wifiSettings.hostname.c_str(), EMSESP_APP_VERSION); }); syslog_.log_level((uuid::log::Level)syslog_level_); syslog_init(); // init SysLog @@ -377,7 +377,7 @@ void System::show_users(uuid::console::Shell & shell) { EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { for (User user : securitySettings.users) { - shell.printfln(F(" username: %s password: %s is_admin: %s"), user.username, user.password, user.admin ? "yes" : "no"); + shell.printfln(F(" username: %s, password: %s, is_admin: %s"), user.username.c_str(), user.password.c_str(), user.admin ? F("yes") : F("no")); } }); From 38f00946dab246c7e52f74395e16e512bdbed03e Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 16:03:06 +0200 Subject: [PATCH 15/66] show device info on Web --- .../src/project/EMSESPDevicesController.tsx | 2 +- interface/src/project/EMSESPDevicesForm.tsx | 115 +++++++++++++++--- interface/src/project/EMSESPtypes.ts | 11 ++ media/web_devices.PNG | Bin 42136 -> 59196 bytes src/EMSESPDevicesService.cpp | 40 +++++- src/EMSESPDevicesService.h | 19 +-- src/devices/boiler.cpp | 18 +++ src/devices/boiler.h | 1 + src/devices/connect.cpp | 3 + src/devices/connect.h | 1 + src/devices/controller.cpp | 3 + src/devices/controller.h | 1 + src/devices/gateway.cpp | 3 + src/devices/gateway.h | 1 + src/devices/heatpump.cpp | 3 + src/devices/heatpump.h | 1 + src/devices/mixing.cpp | 3 + src/devices/mixing.h | 1 + src/devices/solar.cpp | 3 + src/devices/solar.h | 1 + src/devices/switch.cpp | 3 + src/devices/switch.h | 1 + src/devices/thermostat.cpp | 46 ++++++- src/devices/thermostat.h | 1 + src/emsdevice.cpp | 13 +- src/emsdevice.h | 47 ++++++- src/emsesp.cpp | 26 +++- src/emsesp.h | 14 ++- src/emsfactory.h | 1 + 29 files changed, 339 insertions(+), 43 deletions(-) diff --git a/interface/src/project/EMSESPDevicesController.tsx b/interface/src/project/EMSESPDevicesController.tsx index 52945963a..dfcb5bf1c 100644 --- a/interface/src/project/EMSESPDevicesController.tsx +++ b/interface/src/project/EMSESPDevicesController.tsx @@ -5,7 +5,7 @@ import { ENDPOINT_ROOT } from '../api'; import EMSESPDevicesForm from './EMSESPDevicesForm'; import { EMSESPDevices } from './EMSESPtypes'; -export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "emsespDevices"; +export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices"; type EMSESPDevicesControllerProps = RestControllerProps; diff --git a/interface/src/project/EMSESPDevicesForm.tsx b/interface/src/project/EMSESPDevicesForm.tsx index 41baa437d..2f6c2b83a 100644 --- a/interface/src/project/EMSESPDevicesForm.tsx +++ b/interface/src/project/EMSESPDevicesForm.tsx @@ -24,10 +24,11 @@ import { FormButton, } from "../components"; -import { EMSESPDevices, Device } from "./EMSESPtypes"; +import { EMSESPDevices, EMSESPDeviceData, Device } from "./EMSESPtypes"; import { ENDPOINT_ROOT } from '../api'; export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices"; +export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData"; const StyledTableCell = withStyles((theme: Theme) => createStyles({ @@ -54,6 +55,7 @@ function compareDevices(a: Device, b: Device) { interface EMSESPDevicesFormState { confirmScanDevices: boolean; processing: boolean; + deviceData?: EMSESPDeviceData; } type EMSESPDevicesFormProps = RestFormProps & AuthenticatedContextProps & WithWidthProps; @@ -65,15 +67,19 @@ class EMSESPDevicesForm extends Component { + noDevices = () => { return (this.props.data.devices.length === 0); }; + noDeviceData = () => { + return (this.state.deviceData?.deviceData.length === 0); + }; + createTableItems() { const { width, data } = this.props; return ( - {!this.noData() && ( + {!this.noDevices() && ( @@ -87,7 +93,9 @@ class EMSESPDevicesForm extends Component {data.devices.sort(compareDevices).map(device => ( - + this.handleRowClick(device.id)} + > {device.type} @@ -111,7 +119,7 @@ class EMSESPDevicesForm extends Component
)} - {this.noData() && + {this.noDevices() && ( @@ -156,26 +164,105 @@ class EMSESPDevicesForm extends Component { this.setState({ processing: true }); - redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT, { method: 'POST' }) - .then(response => { - if (response.status === 200) { - this.props.enqueueSnackbar("Device scan is starting...", { variant: 'info' }); - this.setState({ processing: false, confirmScanDevices: false }); - } else { - throw Error("Invalid status code: " + response.status); - } - }) + redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT).then(response => { + if (response.status === 200) { + this.props.enqueueSnackbar("Device scan is starting...", { variant: 'info' }); + this.setState({ processing: false, confirmScanDevices: false }); + } else { + throw Error("Invalid status code: " + response.status); + } + }) .catch(error => { this.props.enqueueSnackbar(error.message || "Problem with scan", { variant: 'error' }); this.setState({ processing: false, confirmScanDevices: false }); }); } + handleRowClick = (id: any) => { + this.setState({ deviceData: undefined }); + redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ id: id }), + headers: { + 'Content-Type': 'application/json' + } + }).then(response => { + if (response.status === 200) { + return response.json(); + // this.setState({ errorMessage: undefined }, this.props.loadData); + } + throw Error("Unexpected response code: " + response.status); + }).then(json => { + this.setState({ deviceData: json }); + }).catch(error => { + this.props.enqueueSnackbar(error.message || "Problem getting device data", { variant: 'error' }); + this.setState({ deviceData: undefined }); + }); + } + + renderDeviceData() { + const { deviceData } = this.state; + const { width } = this.props; + + if (this.noDevices()) { + return ( +

+ ) + } + + if (!deviceData) { + return ( + +

+ Click on a device to show it's values +
+ ); + } + + if ((deviceData.deviceData || []).length === 0) { + return ( +

+ ); + } + + return ( + +

+ + + {deviceData.deviceName} + + + + + + + + + {deviceData.deviceData.map(deviceData => ( + + + {deviceData.name} + + + {deviceData.value} + + + ))} + +
+
+ + ); + + } + render() { return (

{this.createTableItems()} + {this.renderDeviceData()}

diff --git a/interface/src/project/EMSESPtypes.ts b/interface/src/project/EMSESPtypes.ts index 91fed67ee..2e510ab4e 100644 --- a/interface/src/project/EMSESPtypes.ts +++ b/interface/src/project/EMSESPtypes.ts @@ -24,6 +24,7 @@ export interface EMSESPStatus { } export interface Device { + id: number; type: string; brand: string; name: string; @@ -36,3 +37,13 @@ export interface EMSESPDevices { devices: Device[]; } +export interface DeviceData { + name: string; + value: string; +} + +export interface EMSESPDeviceData { + deviceName: string; + deviceData: DeviceData[]; +} + diff --git a/media/web_devices.PNG b/media/web_devices.PNG index 6849689677e5a29af55baa6aa648f55b2154b81c..4a9796a7b94b916b0a65187e8cf95993377a1df9 100644 GIT binary patch literal 59196 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%0DJ*N(^nvH0v8bD;B@_ zI?slaVU}opc;Y$v{N>7a|0MK~3oNtkfMV6U`hJsWee3L)V9y`0-MC<`w|6e%(0{OC z-dyU5_xY8$(JCm0O9Carq>Bi@2;Wln)ijjQMLD{`&$)8cJ)=2PE-66l&8Lq3 zGXf!l8OV!y^l@-D2-xXfRi4vS^a$B{q(M9%jKIPa*nY^`h!0HI&yjYI7UY`}GW>o!Ib zOYM4tG(K}Xveq-DS{MI$9(#GAkbNTf%8Js#8)7R6?p6P(?XZ=-g|eiM#>_i{R`=|k zmnOYu7rS;WKLKKY_SV?Z-iIYW>J0wWwYy|}oVU{-q5ch0xhUz=a# z5&!1VIuv~O=p4A3Ey#GHoX=@ul_P z{u_1g71re1z5C)6DJn&gE+9&gE?v5aqI5*0SLt0k0Rm1?P*Ebi7ZoYeOA<;#5drDF zmw-U1A+#hw2&@-p`M&?&2m9J5>srgRU?6#)I>vbJ`}g=J24Tu2S81CDNpssN#B**H zT-cM=;7bX~`~zfHd0*!O=D`T84pP}pAI-ln|G{>Cd3@s=`mT-gxF=Ywp486uppSU$ z;d4B3Wm0@*jx++qCJguWDb!s@bU!BQ=M!fa`u9Y0esRH(IKbD<6~_@s9KSU>Jxis^ z$|BI-^hyUR?7$zopA0>k(uQyni-e0p+{ViY?6<%l`w@Fw(&sa=ZX$0OpQ3K8c?U8bu_pN) z*;kU~;GK!aK zZ+s4+1u^f_b=3d~NT3%^57D!~Pv|{5((+$YCX-@Tn7nM3a94@!7r(+$9ciL((Ux!* zbmT49INWrSiSw-c#U32tknfa}+~$?aV6^8%vge02(rA=@3?y#+-2w6mhwe$(NnWlP zXr`i9tEk*b>->3Vlyliyfb_3BYC=c&al8y9RrD_y#^B=XtvOHKZ4hF(4{iWKJ432`9tW zalX&w0{nw={5t`il~P~b!!O1ZIT_9|di|QH1`V9=9_OK&QciJr*ld5G9kQ7?M~7m7 zh;?zV>}mepz9n#aKi+^>`as#*EK*G5ch#ov!GG(>0o?CiiLCnpxDSxMyNB{kX z)+QrJBHd^?H{;(a&i>nfqu;;FPJbmffA1c@KjQzYpOEMy!sgHauAr0sRnYF)$MGWKaHmkXD@) z-9GZ7e}xZ-8A!CpwBFY8v>V!04#>67IXy5m7wnwb8eH;EujAuyYeU$}@(Q1SPNh06 z4QK(J?7L1abVLmwegmbs-=@sWI>Qi&ZphvP@I$H7vOPb0XgI>?$dC^3sMoYV zm7f=5&;Hoc@?QHg5&oyIL(-hv_3CqEq%yJl13;-#qd)cLxxxUD^;&&yy*BXWWvNCz z>ZZ3?0mbPQgIZiyT;-AksVge?nZPDzLZwLdTT3~C%x|E*!2I}et*VCG(tFn4f#0k0 z`FP9I`o7zLm$+Po1GZ)?_9viRUw>W}neeG3v1g~iXF>rF$oju?wz?Ti<;04_Tu>Hq z*d$o^+nNortK0uTs8q)|o-c58=n-8pK^hrkRHGFplAs9{_LV(IVQk>@a$VNRZ(}v*3+_?xpGu4il?ztP za);|qJhvn@-d2wt1$7SG{Tlt@=W1+8k7#0^L8->qPqB;&u+&92@m&&Dgbu=5phL3Z zQh8PKHsEZ_&3&d+IO;@-_fDHr;eTD_Sw4+&aVuU?)2F!;Ss}UlNBM%|owdi?p5sBJ zX(XpJ{j2rRtYD~lXqQoHm%&iDpsz%>e!kZs;2}PG+ru@+z*@a(uRFALT(Odl)R}#}?sw_zD|A-?1)T*@+O%r!Dyt zJ>oPRKLsdJqI;>MkQYoaRCn@jGeomN>}}Y?klJ4e~5SS5EIz;c5lZg#GO|?>)Px`X76EJ1)!D zc-TJy&h>*8L?mO-k@?u9(p!#`srDBg8-RlbnM*%6kE8`==rbUk$$&;`k4d<{>lLS- zSQ=hgjbDP=VbQE~1FY=!H8ZdTpTf^kJa}tA2jEt+}s7ZXNxlX$G9 zuz!}FR={EPOc9NX9L$}7m7WPSJstK?w0>2`@{(#~^V3KjBqr*!YI7Qov(kE0w(8yb zpC2c?5~5uMy=k=x&Rl(n5`LWll@g5@em(9?Z^i z1Ao^nA-za({2l9x0x$RWYf_sbv z9CY@&c{fp)2FHiN;LVU_d#hc%!`mzqX;A2n*253|8A+`^M~I9SpO(zb3=%b}KuFPX z^cXzvQU2nEZdc3h*XK(H+5kYvblbqiauNjtXZBq~Fi!<5XM{m?ClZDaR{BqF5v$jPgF?N>+&_V4Cn#?8L1?!&-f%it52s%X7+d*Do8BLH3Su2? z%Chryby96wKyo3?N;3A5D!~&<6rWcv7aRwh4-sm^-R0yLdiK}^K;=6Q@j_|7QJ}!A zb^@7^YIlFWaZb$Gu`Q zyFUhA%}WxRE_EypSa#WExy5zg$s9K1#AjZYl}x>t>Zm&*XC)Lt^9Ai~{~&y|j7b8G zHEMci^Rc9#UW#`OgHC;}%&_2u^76M7>6@_LdIU@FPv+FSIDEqFP;;MwM)eT}BGpoU za?8Bhzz^GhdU;H@ue0WGCN^DSF3iT}ASov^>u{Q>l}r*{c%G9lCM=>kTZrzF5VD|x2HMGWD(GkFvirfHR(30)@{rjJXKSPzDE56&Qy&F9QgoXwxSdk{ zV#Gl1B}CuwbUh6rpqypc+7!nuH(fGQzM2^e>#AUE4+>-?qNO2Dki%)tiXNkzvqK0b z2HT&t4r5X8<0L8vMw21O=_$!QgYigwJQ9O)S{mn4FnOZtm@XZocTqJmvTzGRW3mAW znF7BLyTfM14+XTT;F4eH zN)po`0A9=iGe9Sju*`DDcA0+}DtZYrJV^Ke&XH?+EMz{{l$>d-d}K+l^a*W(JemUc znR7WAxUs_$_=(g$ z&K!AAy2zG1?U72rz~-^e8%EPUpCjr0mVvdY1X>eTJ>+!Tw7B1~H7rej%Au0$1BOJN z;}C8=n^5q$Tf0Fq`1{h9)AMG)rm(n|O)!Z`X5{iMCa5mrHad9B~FJC9sa3{*3(q2&ne=im8W;dn0DgT*#m zjS!lL96#?SxUe^Vr$5^|jqsuc(SFowK%ZDsy%kkLgKO=~bQ*&^S+&H3qKG&|lvdwe z|Ch_}mI6mY^lXx9j@^TCkok^=5#EG32nS91*Yd3X;2S4dGGu4FDSo(NDWa)S!)mWc z;P7ix>ZTbLn|80~DF$^?s#2g_H#-|yG>L&SyV1Bzyfe?+O~TLp;P>qfcc>Qe(_VH8 z`Cj#Mw#<5xc7-^N7m`xd8t9@%CO_C82SHF*Ys`|5PXw^z_$Uc+d{_dX@3(6KPy$`q zLiy&o@+3`@g^QeO`kmEWM%5EE4(znMnCtuEZ^k^d27n2D>1Gq7(J$%(CAo{rT7wLP z$5d0Bd+gL2lp8Tu#PLe=P3>WqRA}f7G_V!T^hU{+n*+VP{i ziy9&2McEb|fAoA*I`kaVnH$(bpMW23C|ZGw!bExsf4sSCNH_K#lj79cO-4+rL2Vs_phq`-~ zR6|Ut=*K83hbUx55SonpTPED7cs7ghbO3?{R$d94*58)f0{4V0IZ8m?7%^*Jx| zZ%H5>wWEU&6qg1WTw_Oz`%k%R7ASuPtqud-+Pl|a znjZL=WT04*|RD%T+rOqQ%<|V$Mk;71+2%gT+dR<&K$k@PK`Y?u}A)m~MAZsA?C!N}n-}rmG`1 z&1pXrCUx`a*7CLISn2Y?`w60@Yyqo_rreuUX8s*wN>?Vm_W6ib&ZE7fUOP_tKj}(e!UW8*p(L~U!al_=*8sTc=dfU8ytMh_JbB4JEc;;Rs z9i>hVkZ_)eYQ96Cm9gLJC%-mHGKXYQi1V&BZt=UK6W5Jue@$0$*;J>}U!$f{MK;fu z0Ez-PLK=fWp#8-5&5bD1W!f?3ThG}9RZgf{pD8VuHF;g7yyJ}$qE4#(Jqw{W<}q!t zbb@S0HOhUf9t!jAJ{oAIp5r{X*BjDGiyaBg+o>EcRn1wXFH_k6C}(6%`D;DS72*7z z-$Rr-Fo!dMG6UF8h+`+b9=D+&Q1+m zl-LWvO6;@TsEkK4hL@%*Dq|Z2KaD8=F4Vx!YbRe{9gCmYJoCD_>3sA@@H`+ML!TkKneoUX(eG^xY_AiNJ@7eg4a?{0mM9P+OSBNX^1;AX@V9TXn z(eLhu2Ff-6+<30CqGJ{T^UkD_&ZOLLY6KK8QQ0jcdgq)cpvNPb+CS7Zh>nt8@bgv| z+Kd{1@e7#uW)S=U<$n)Kr;PgBtpEP2UlQhXZvJMa{MVnxm#3D0f9?FW z!zk$Z9jc=@Y*aVnT>|l;Qxm0LdknQ=?XGz z0ahaGLntzB;(pp9D8YWzD}<=`hP0H-)VP%yZ81dLdG!2l!i&&d zv)Z(T`c?+CQvPfV&O6n^dQD^;^I-i_0DB8@411A{_Pu@VS2Q=^$MEnjf&S@hFz!ge zHAsjQ(R`7pEVqG~NfH;v31*>Z3wKi#f;L{R-o-Hg*sb=FnnJbiLDnyteFo24B?-Ek z!D?phn3IulAa2P}Zk$Ixk@oNBn2Py{Ai%UIn`bC6Fzv<4ucLNWhv0!)q%+kw|6vR9quf%q|eNB;aZu$1E{4O%gu||6nnJ@Bc(Q@*c zc`0%e7O&#D5~rMJ+vnt=z<;q_{l#)8;%=31lV$uo#VD;KdRu)oE$ z(~9vr(cDWc(T{Urg#xgG08ZKxr%CI+o)9tb5p|UtG;_5yUs{P@s-pQXfB4cGa;u?% zQo89XV>0V+6Uu5}O6fHU%Ud(cVOj(8=WQv4Uf}J_2d>uaj1%?E0VynHHt_4lSvLFc z@9Uq|f3D9GS(IoOOWJ6!=U{P}%={&cw*OAea8`jz{gm^}3$!o63~c4jRIcu+@pV2V zUDsuVy;1!eWeTl?E=ZsRhti4qoRj0bMmqM$*9X-8!FnxRqw$21Z&QHW|Di9NeNKqv zns}a+@o6SpvF`_EuLSn>b`^El>uIhx;+1`i;VJw&M<<%~M)nrK=m`qd?3^gg_hdlm zq(J*5uS9tXQ6@!MRU2+t%V&vdoxCct@#7_zPUmz(4t$=(>H8y12@(%S-(Ab<$TK=} zm)T4acg^0jd1X2aktl!qwy?Dv-LCUNMX%ZdwRVy=zfQ|e-}R8HjvfV`yC>_lHyRSoKh3lgQO;oV zTMu;DHhb7NzTaVA?dmW`)OGa=QNBNrPo=7WHkP463)u{x!^@qH=f0 z484RR^vTZs>wA(Xr!fn!w{@yMBpc)U_oo2zh0TZWLRK&O{C~N9HULec5g_dUeF6ZG zF3(O3|Np-jrQ=?uRNg-K`>zb#|M#fE?5~>|&c3gW&EE$V{~KQ5(^&=HK3;nn7~=Cu z&;SqutNpegk<|m9zvgB$6>ewEcKp&;i=-FdlK0{#6Akf~IVyI(_V9?fotgGZVl_Gy zP}BitWg7nC{0yz0byt3a5=ud7BfN_e$Z*|@Xsvu$=C)FWz^y}{igTosw1m!|U^0_u zPJ3zY(5?KEYbnRe=%6QOZtXzuekx#-j7yq)9dFq4XJgeJR`cxm&RY&eq9I~M!i>BP za*U>)O}8SJq2I0%KFD`hx&lkKd6;nrbZQR3=ZYL&0z&9s+MYm#-$oC>qOgAwKhhJv z-1^jKlJ{2p$!20twv@H2*5MY|6lmmoTs!v%Twi=w$V~1I=+T_a{Aq;lX!S{WBrQ;? zX^VRG-t!KB_jHxP9eyR~(EIoD(?wZ=8jU6z2b_0tXcstd zgUjr7sv50F2iZXA33>QH=X}CEz$35c+ZnJzlHX%bMTo;|9F9X~`o| z-knK3hmq#8O`mL1m+viCyqBLo^rPRQM!iK~vYBz88yT7Mc|H1g zDKQOMzdMMFdo}Tz_Sbv>Bc8Yo>$v3 z0?_KS;cly@$&(*&%flC*t`zrlriQaCIZj+}#h^4$)B^0U*5n@C?NxJq}kC zT6@9arLf#U)oA>z8#~1WG*c-IUVLM8_+gqi+wf(BML>T6IFKw~<(i|lpUE8YqBXjg ze&^SM0$&2t#9-+~`|D!JR+qFfF^STjN0w*|bH=3Z;dWl0Y@WmMN25Qr59d?nqQnfv z21Yz3B^BD2syuMYyV*Rumwo1Lou>U103rm0mGj~8| zEYzsV+YXdytb<9-K26~Y;^ZGl5v#_ps{5E-U30SSGjN*-paoC6CzM$A++H*!ro7E{ zw?8E#>jia?4h)szintz4%RTYoO@eo9$)L}fa9~3D;K3@n%$M@Bp%%#&FK7;pHl~ce z4A!T6D7CL3>3A+=yRg4`WERohB^{O9x%VO#x>rYA!w~_iRB90M8sgGgM-q>KmE~-4 zWplnP{Hz_FUhSlSt5Flj z`P|M#%XP9$I}b45(>W+-@3pnLsrLG!Vn+;_Lcq{QH2m^h3Z^#D*Vy1uj6GMke?~3+6Iw>0E-%BFg|(iTVQ1+qKB(EG z4NvA?HT92%f6*Jud!YuNJ!+c)G^n|f9)Pc|`Em5~^A-;%)O~oeOrY0G&NY8?U6f3# zvW-4`&n1u^k5i48KUEAFXZ|oIHkgtAI`>IHu5f{9fAeGD^ZOkY0CNCl)-{%*^M87Vf0D_ZD-+O)iA@@P>TTT=$1xKR{Uf_{!mq7ibm7bMF7&DJc&7Je*6e$ zZ0$P@M1Ft_$0$`OzDZDIt-{}BlSht)rRXD$QZ_yAGewOD&HJU-RaJ%`dmO95O9 z?Lj`e+rSdv?aZ9duKs;lL{0xGC>_ciCSm>N5&!Y>2a(r6NNixS+qD*E80+95Tynh} zFpje4+m~Heq}qFE-WLry^GV1rWNoYSkvvU$Iv?;<+(FSq_YTgexOe+J6XgHp5ZY^& z;@2&$;7;<8rBu4xyZxPT+uT& zeNkCkyNTM@rBGK|4qMl%mI8@b#*(stCO3wrVo3w3Vk0!}dSA}?^N?GzJ(@0^)~P1p ztAPE1jAHBv=%usp1^`|EC29N}>*NAOh%>$>{~5D0vD;w-Ky7FXQ`(UJJ+{ZH4Io1Z zN6dnSdh)<_{}T85Z(msFjiS|j_gE4>=@J{X$Wx30`mPHMC_=0ewRgUX`GIo1dR8NkUXai^rJW0l5Y)A z3aV@*PFq$?3!s*JrX>V#wSdoq|3Sx`ePgfsHNFp|n+bhdg3~eXRIcVeXH)ThTGoZ? zAIAl%g$xn8qR!fYa|Lgh7uuD~ECAd}g${9ifGw-PlUC8=tuq&u`}I6^_eWta>Qf^gP)Oa&U(nXC9qCUGx|v&F5nsODH*_R2s7vzbw>J;E)3f_qiH+&pCYrwOv8 z)r~jZk4Epl{=X~~AkZ!oX9q}s_uzeRW#;`FDhudbMog1u)WhdL1Lp}>41oJ5{<+C5 z7cj4zutd;)wUEXo0>_T(IFeVX2;c@iq3z)dtaf%w9q zyA5!rx=^V9F*Wf2jel~l3GSvf zc!W1lxbz%GUk7vM;J!V^{7-N`3g>oKB-2Sp(WyEslGFV0v%(yag#wu)nZ%a z%yVMX`%lg>3Z>uoFAvE=;GyM7mqcst^{JeO0$Z9}?7y?|Y!&{8bbgk5v6!5tUckS9 zUStFJtvz<(^JXO@6UvB-2dHQ|LsfT0+!}po58%=KJid2s0l5P3zb~Nx7|q6}G@9N| zyP#!NYfO~ zkaXz%ZF$^_G4$O{sHoN_4pvV1S+Olwky zZmPtUD)A?HoY2SB+gs!l`ZZdvm$-Hh1}B#4$)sr#zf7x&X&xwF%KwVcuCd6j(J4u1 z%Gr3#@m+g#XtOJa=;>7s`P){(TUeUOm$?c|m*%v0uY=ju9iCgON?k0x&PwP`wM)#t zEzHQ%;9;o#T|&BguFx2rjzC%}D+;*yUKOwMFxsE_BEjWur+KlU*L&+hJH?k<=DnTK zqAH4Lv2ymD@;BG2!+_k)MP$qE^7c@)J9+xS?0_;7B6(T0KP}Pz9hzE#A3dCs+M$%0 z>_RFl26O@`l@M^9aP_o$pR?RR&QQF9i}I^w)3k})Pv{yY<-a15gvDaTJi;gsz z^(RH(Hnht3T)Ow&nV_QX=ppLvDbv`|!C$^gnO8`fC8FBX^6DG?U^smI4484IYTb~|Q)Pdb~VZ$6ElCBA`9ux2OLRlH5C33^k@a%VK zVesMz+0tu2ce!jyU(BwLh+E?JIKfA>SM_+lOHLDAWqlRclYZAV*1j@XDgd;7ezY$_ zJx`*cvT=s9fkdQ2gSa2+TvUjho)fSC3~KANC@BftH5+`r71$ajkR~G6F&#C=@nsMj z^ZK)4CcPn~GLGx(rnkHaXGr7C?qh$*a?j43Vn73FBJlO0=$lRR5~%xZ&9!7#gBffn zChtji(gg$Kr|OgLm6g=SEKk6JrIl+e{R zk5se-G%n=8VPV>Mi1-3TZdJBO>EY89S=`BwMfZ(m6J8PJakYkv21=IuA#Z}xvzX0h z=d5yo-Sy0Fopp$g>4V*y`k3q;^4(Lg*&MM9yKf<+;koffPL7kH8`g>m;;&(Q|N8*D zrtJflw$UY1)+YN0L(|ZPFKNj_zzu(%=BkrS()mNzY6@AATG-Z^mA;xH8={D3&$t&o zo4sx-!}F$xdfRSx$gfZBv<50$(VUUsv6s)_E3gN-ytiMLJ*HLlcxjrJAif7@e^c)e z1=H2qny-1^v0^U3P8G$S$yxop!z<+eRh4{{{ztRC(In;wG@W!*A*>`VrJ;KkU&wrt zl^(eJOxU5BVv`YS zo2YCOAZ-niq)@l!7WW%vyC}0hj^wOh%!`4kU6muZR~M*OjFA^BWB_%Lk$1bg&R=Bb9$aDM~mpG)>0(d`i-jb2e_1PJ~mj`1L_PRL=is<~RnwQ=U<) zrcRYBi0(D?^KcF+{?h4)s>)<>8tY7faxGviA^lyJevCIjoVCuC|KiA;nYuIofX_k& zsU%cq&QAj4#{%_UJ(Bs*Il)JBkA%5Ea3V|s;?x1g9YBv^7xH;_!J}e)f%g$&%osOj z0&ok46lpBJe9DD~w<+x~nF@5e-Yr()tDCOu&|C={H6n4lyDCfH;J2K-ijk3^!}yV3Apdz(&;d~Fejp&}Va7A1@g zqg9*_?~M`6UQfK5L4L{VhgRj711X#A)pDV-dL&XAz%rhduH^S_@vm-aeDnQ-HEo7r z>WI1|X`6SF{*MWXEg7~3mZ`C)C-N>dj5TgV1Jic^-v}n67+E#<)@3O#BuIFQV@|g( z3|4|Te@`JkHcq-oc!W}YU5Z@CW#cg27pcEYk<^Q(ar#<&{nDMR#X3wfSIt!pJbHTL zRsHl}Q{^ol%v%}fd7295U3|M7t4uW|<& ztc>@U#x)P(6#>eUqc|*9?k$Zv50txgK_lGMj>?rQAt1U*0t04!wAJtY3X=kvj}LAsV1F~U z5m&|%Q4bj;h5*3A6$wF(Y7wJIFriU)l@{XP5teV}@2dk!*}wC2c&k)s?91_=oor=l z?;97wQy4uT{~obrFy-+lFxtUQ1?cqjhU8h44wykCn_Q^YKRmvU>07d3Q3=>}n-uF? zOX*(RnWMJ699E0Hk*wr`;UTA?P~WAPsyKg(W0WWb$M_c>U)sybOrqwv64qK9_+jvAb-=(C z8jej&TS=an#`JbGZJwbtF=)B)y;WLLglFHZo_S08*Dlfj%MfSfWsr;hXpk3z&JaEK6FHB~8aypyCs9xv)o? zb)QD5|CF{mMN2MW+2lO1yWpd#Nnu&ZHDlat_2rPRk>5v0h3fDNN9yOLCQJC+9+qw=35#z z$s#DCKpg}9(mI~Ec(!f$3!X~4=C423iD^euz&JYAk6}9K^R@@mg zv}9jX0tsZLCUq_>E#E5#+WGhQc}MT}L5Fj6znU+}?r-FtW1fCl8t7T4mmY6f^;A2p zanFgNeSp>-F939*yLPYU`heR%8R<=O%craYq8VAjj=YR%Dx$vW3dv5q9BC+kSTxA> z&%4%(=?y961yj1yJp!cG<%v8Q=q( zOPY;J1G>|9T%)^0rbD0us`?10xqQaJShvj6qB$+_+A znE$&3Z7o=T-NXTK@SJphUHsj2xv$UcNtYi`eOqwSS3mWXsheJ>mc90a*2{0) z**y&vKLp#Ag2Ru-#}!#bQl=C|?O+X{1h|MEBiA9U0RxYUG)0f@{%VNaqIMJd2j<{zGJw|9SJ zgl3ZVyH<3a+G@V0M>u#H0bnoeUBg>qPdF1@SUAGQGgpJO&Q8vzJPy7<0akT`8rDhu zVu4?V)71#a&0KByUE&XzcQ`S7r17(-L0;-tA;mLD+wUj+i#T%rN(9~`Fi5NXoLS<# zl#r<+qteGa^ACg{Tu=Et3+448S0e zt|AEK6&Ska^t)!7u$nxW`ly%7C zo%zJiK9uG1e>wjDFaOD^8P!pgETY9zm`V*OE|oxWS0lpqqKED(5$!++bD0*&`e6?# zW4qNL$vmt6HE;TOo zbkE!(khVK^dzudEPEcXOTKv5sQv&A~wTR2j=OM2!gz%@ATSb8ca;vO zJc!Fj@0FPeKh^rvLHtq9>J!2{Cy6$Y{27z2vYcJg$R6yR_tf!`XJ?|vL%tOW7!tWnFBVY z<%xPaw->tJ;27St&p3&8CpOdV?}&6hahih;yIt9aUQ}hbZY`@mR|yjRey_1}`h?1>*b z4pmj-84uB4=0bigQ0`WZhemmxpwFK;4b3>w-)o0dmraCVV;sYwn?q`-9avzXM)JPs ze)Q!A-H?uVj9369nX7lk!1?W=G_wZk!Ed(J+PZe4?p5A$qmOVMFhoeQ31&T^T8I0U zM7wlOP2n4{o$-^&6yAd?!*0-LbUg3*`qP*{i*(L94f^!K1`_y}8^LoJul3%k)~a!l zz?IhB_i3cQro2q%GZaAfR>@h&?IM zl0mmq1GAR8(BdZ|Br2e=eSPaeLYYw)uI5q*6kvmjp$gtm4*((w0hY*$dOhq@iCOiWTSswYNwEHj+ z8T2yc+NuufRtML3b5BNg@Nr0$>?RsBi+#$^_3?95WJ^6;i8Iwe4bF;wDj5-u+qr9N zYoK4Zv(d3J+YIeq>qJ;=lrLTg-n#aR(=!)M!szx!z%sb@|L6(sZm#df{_LJ5%*_T+ zxx&3Y=3qlZFK&gNkOpAl&NQJ9x5`rgSVlA{3e~lBEG;aCX_YtcpO<-~aWS0zyV9d! z`Vr-9-a2dp1Z4TCRdlzKFkG#Y9E~PK6FJ?ns|)d&L%I3|PC1FsH+Rp?ZLEsPVmqU; zq->9tX+`o6v$Ws07LOWBnz)iV-yz{*vqF?iDTuKcc_k0d;ZJ$VWzilLYp~C;h=8~% z$I0`0U6RmfUgkdY!>~7uft1ouUHj3+PE?uGI-M&S?N zhUX_(>Gdz8#If0rE4GwSo#*B-yU-Ab)>iC%o$KzQY`4)7BDj7hY)H#SQCJJm`b%8H zhs|)A9e08@tw%5v-3<8h$qlB3CT0(oyHAf^1&o(B~Z9gGSi;Vo(R#QoPyR- zm&Y$A=XQrz_?m9H!_Y9KbM~~tx{MS}xs({i1PlmDU0m)$(W55*KvYJ`tjyi|r?JW> zd5XtbOCQB8u+gZv4`EvE1)L=*xId^?#E)iua}J+q_)JM3Zbf7426NT9y4yFZeK59O zb46I!@d4xs?#V0}hVSq*h|^v6#i`F9(`9;|gUQ#WWC!VGEcjDU3rc$Zibq*@ULuYQ z$9r*~!bgZRZrNPbW*FN;GaeS738PHUOGta$OBLq7y0g(B^_n})5=WnzcNSZ;06a4p5MnT;9 zif%BkKTm=IYxREIzee_rU>hDu5v8D7`i7H8IRl9!cjefVlIXw*sa5*VwQZ(05tOa{dHAKY> z?M6p3$vzw;WR4T+kDP{cQugb4CsW1C;aud?=hm4Z-QzQ=af!2(E7Fzms1Tat@zAwb z#IQhVIU}Aqe&D1lT;m^Z2-K5Oedy1iSYoMSZ1H7p z*jgPAv)Mw0XJ3UgUs}3~(mU~;b^+W}S$5dgdGPhRs9(m2F?dUe?%|&f$G$@WR8#Gw zn!T{6H2u~?8}sm)_wEgCzS#&42n}B`s+clLaN!WQRiHd-osric&Ym+aip=?*oSPIM zy2n-J12V9w`@R_1X1y0+E!(f-gFNhE<|>EebJZ!TZ7Wmb&80!uhvXtV#%%AuO;+Aq zQwBDeBkg}Qk0m&;&cMd^F=JS_{V>D4$^11j=DMt;FDeqU8QOYEsge%y_df<4sJ>-* z3?2+H$JOsYI#1O!YLXyu+2CqxWl-*zRqf!x^$^Q7>G^&2XMy*V`1*0`AskIXKNp&G zcgl*zRSDzh(t-8a3Wi65UM!pWPoDqG1BPKQflF(5DM@F>O1P$~znJ~L*({wZe3Ge0 zhmZf22ZA%dV3xini>MLSG|MB4A{Gj-Io*(GLm7W4hs?q|Al;68feQVgY7CF%rP19S z)KkJ3${6p%fG2TzyiGqo^jh4T`btPR`n7<4SYCS6N_n-clx?~%rDw(DQ3C`Ec!qeJ zJUs?H#FSmQtS5pc7gz>2k;W^>Kd&^siu|%xd2``+JL&wVoj4-e@1LK(YFJ**EV9QB zZ~G(f;H}RljJa0v#yIg@Zg)erBjNr)g~DbvqzfEJ_FahBL$^yq%xz>L=X|>`k@h;* z3XWM4{Xw&!U0uQL5`GNocRsjkWh~9N`fJM~=gzl>WUe)|Ch4!VyOgshLK{$Ml3(EB z9+yS9w0B75Kv!eVFA=(aRjPHRC|Wk8BG(ieC&4cW4TmB9!&YSVzbZff z)kJ##X`;ESBLU&GW)T5v>A6-yUwG>jd~SwiSu}{Tw8;sOyF3!+p*lF1lJ&h~RYeBL zLH!lqg(GGTHZrtU0v8uVistvHl42;6fCfsaXlV%eHhp!34pfXv$RPb`syoZhA!i9X zXPV%4m#yWc1lvP~?$!N4JBd8&MVA}2%ds;F0wb74Q zZB*?X&ZgEOmH((kmZ&9s{ArEiDCyo+D|kWnMP`eCjg-94dh*%h8C$q@DR49e2tJK+^Gt&2C|Aa>{)AIyYEfz%LJLU8C>Wat-r-am+T()$s=OI?2Dk;^ zK0|uG`YqW(@~zq)qSdSjTCz1R^76-Ox_8)Ao_O9hJ&l!354lWwX@wou_b?``H2>wZ@C9}w|AUQ6gmP>0V14tgU_uM>#ZT}Z@ z?-|xq*R74BqM}Ig0YyMS6hwLl=>me%k*0J|dW-Z93L;7o>4dKIj#McD5tZIMfkdSf zLXZFfLOC;_@B2J^?{m)n_V=CZI{FI|)|zXsImfu?821==mj0Ltq~CX??krfE#TfhWpc8wu5THG}<*cafKWCnT$=|+&hs;3Jw*}X*nb_w+}a?&JFEr_SHWs@)V zZ2Eab==?9~4TeBs+>F*XD%=!K;d zScBa@?O)E*=d8aA9$vY)f>Xx8zJGLGet`=JTf0|_EA}gXg(SEe8RdVun^L*{?d+_- zWWHbcVs6YPYQJVHe^GEB4&eFYV%5F7VoP7VcNxVFJ~I8axG@Y4LO(L+3}Y$p8fw|8 z49E7Ing2Cve?c}fv)Y*J+4%P6Otk%>6gKPL+$x}nssruYj47`u>dhwiCRk~KA;LY) z2p=_oB3s5bx~+ely6R@G_pgI;E2iW5wc_Nh3<^(b&t}~W2}l-Fu1f#*+-Z5Wn}<8+ zPbSzTVik|KhW+`Zhs*#j~?*$z39AEYeRI=~`PS8)vW-ko>~dbdt9&c2m|Z-5Z>Y-{3P~5_&m_DU-z>5yg#FC4cX6@d`qblVe`!Jt7vi*l*-E354>RT zyy#{|VAkBxhWh3xWJEQ*GV_35IhoxC>Wwz9>l4eB0MLgw3Zi-XPHbML3-|8j*{_tB1)i9#T!p(F|Jh* z2lM~+l}4q`XzNHx1vL5@IF?~4@&vTVmiIb^2fZl;H99dE$d>=L)^MwWOPQ&vGtmi}qsSth^!4EXIOk)YBIm^ldQH`|3Qy|kQZaEWPf*BVqi z`GDR|8HCU`w_d3-@F?k?(oIV^Zl%kOnhuo!y0ES2Yy!(j$uJ#=E!`ZU(x;^-Uu`q2 z^}EffkQXt%1(+h7W2Uio-s(47H=q57R?$EUM?-oio2gbIFMBkt#~^Z9#D|ku4b&B^ z>L1CZuzcu#8_@@+*0FN@Ua)7jVbOagYe{|*-Fy?2Gl1N&T<${Ge8T;?S52~t7H(aj z3CrYPRo7^-ILpNCzLs2Cc4$zwws(gUJy3!;UCCy03stSUd1`nrj2efk$+MV$LE|@S zHyIMBnA^RTOP;W@Kk`Ox2Y+_s%Po4GZckCH2&nQWoS3Hm+eK=R^p@2OG7f5tqif;bp{O{eMAe*;>!Hhczt z5M?DFe8embr+MPmWQRVKq*Y1AVk5Y1!`SBr2?y%8vJ~9rarodjN;j?b$$ZcqTFc4C zy2YV@W5<4@h9)S1{B~FHrt-ihUGO8be>O{(C7E;(}QS^>lvNFCJaD559bwXs8==Z*)3QBkM+&$_WGodEkjkePUa z)iUYN*Pazm2SuO5*%g!wakm(iMP6pIcU;~VP!At?_wE5LlFleLj*@ym@;K4)XQ!T% zO^GBgi>+B7<^!^tJvXNnA=PIX;%OPujSE35>5hN(z2;rpRP-!M)IejI4EgDo&dV6D z)jSZ-zwt3@#4t!at2&Bd;6zlR*0}J^nUwt#38fvrb9=uVVTaFESS)0D%zx z9f#RB6i5n_t5;Vn43_BL+utiYv24^gqBhU}bxSNH=mzU{0X^A?QKkW`u z{9bIgaUj)t`kB?AEaU$lGspk)v28RQHCzja7REH^?nn}G9?4>j-Xjrb-zhBE***jjx<4rLGR^7MydoV{m_-Me+z@82|9rHido-f*@6sV5+a zc!1t8wu}a(zM9&5td*LjkXnO;I=v`MZN2k(c8Kn|ffvnOq z-AZ$1E0xW9k7^;#SlKRhI7th%$|*yLw@HoY_3AJV$**tvYL9e;;D?6Tk%=0oeH${!$Q{w4{2Lm&s0&eI;(T=KF_;Hsp?Pz<9w zFu}!fiNe>^;BzMJMJz^#Pde?^Cq#`X%o(k}EfthFOPV#&#hR|9ef9Hwui}N`xJlftQb+9I+49%P}hT=9b%c5#e%Y!fwSMDNmMF%ueAM z)V)s>9wSiQd!Alqy6)V>!m$UufK^~tauG-|+3@jc>08y}0jH{D+>Gz)m5wv7FLjv4 zCsg^S^*uv(@@UoTCfaxP@k6C@Qu?1qDO$R<7002!qnt?WRzY2&YCVk)Z;w#(R&EJN zaz~wX9=ZZ@6Em54Aj$Byz#wiVemK=}IUMl*R-A$^UD;>!? zZ@k{|9oa&?3>m%{L(>{vH$eXHJq@1!R5+y&UC`u%;e|Am4G)2RWc`%6vP7TvYuxg{ zIqQIH+|Z!NTdgNTUW!=eYijTKnH6mg~CZyUW;&Ta^td7|Xlo9w?i#;;8%g*^Z&ZXnuKep5sh z>QiFcCZY}Yu{Qmbz`vFQ(CI>3>hyxuLPP`ug**gwnwGFN2KhkvQ$zw=WeL)9=oI+Jx79NUp1@3=um=g}%um+?5SN>iTbdF&o=B()Yg1HU-xn27YF`vcwihkd#W!Vm(GunhPw%u(ew0`(UyCt3ttD z9W3>|T_&)p9{thNz5`YzY`;co{UOtOL+jxeEFq07c06}PnV_$Y&kt)*%JHoFcgmjV ze_X%~h$Zr+9V#PxiOo07C;B7el6fR(6fNxzfsm`R!5J_$8wtOC*0TG2wl|U95jU!g9c}P>K$N1v;(!&1}$c~sv~5v zNTcLF2a54dUm)Q6Q}+HZGPSjCb8B&`s&u9`JL`4Q?K9lp!4jDG1soGRf>s8=omDn> zpNX@C!h{#Kp42={$DY(_=!Sw0s6r@Jo}@b9q{A}JV~@p&h7q3SX~1xN zZV_WoX?x5zEmO;i0`=YMBelk$x;y&mKB3PiDyjrHKQ!V=%D~HrKUh!xxiT>Y7@K>=@j^$PgV*4APR5SC}DE4h*TUU)UcM>eFR?EZ*)^7+IX7MK9r zoF!L`9SSsvT-*#}+yc3b-a&^Kvagc{PV)L5~kc?{lL-Fs9zAI024#r<9S}5btZnC*SXwfQSH4<4zl}RNf z$o;Nu=E&Qe0<~War31b=1lC>OG0uu<`XcG+K`5I9k5{8;saLd=H*Wzvbv6$)VK1h@ zCczpNdG0Sm|C>Fh`p+NvOdh%ZORZm@(Ogf!te;Y{Gz?4LtbsP8MF6FS{}!e;(9 zMD(1j+8h@e$w;T(WogJ$dBR5XW<_KIvyD)DHQW@)+wVx^FwX2q!jTcP5o`zvhE9H}+?XyI}S|(S86ziQSIw^Nu5m|448I&SP{lHXcL9uP(<^b6__-#046{vkP-?`Ob z)X-`Q-71}-Z3aDo30w317fKaLRv|=Qd9A*GItb!?8>$|9X?!PmyJUl?wmqo8G!y9OW z)W=Z*y7O%*TxY?}df%4Z7-C?2#0{T*L z_OM-68L5t-hDls#g{(gNl&G4+l^D>zZR?lr8?P2{z}J5y95vVZfEFged0OHztWt_T zOphf{cXA~VX6fL|oPRhq0@hFP^Jp;Gel<{<>+wu0(2T{fz{-r}yU~%u8C3xtm*>Vy zP8WEA?g%bwg*jp!4lZ30?Zb_()7?Xs9T>*Vgz5{jp)5c*s0;^p0>~Aj)L>ud3PzpAZoo&3=3Py*UjT0WM4+!6W_&>=5aImFs)(i)UvUY04dV zzy{vC&#B+XG=eugTn^lO-FOIVmiP1`lKK~8wKODi!a>gixo5e9it@i706II|p#YC? zbU)yR7Z*eV41nTAV>jbRb?IOC4x|5rpI$6G+8*H+-ZPrKlej`4?E8CR~H`!orB_Szu?a}Ydx8zm8S2(4e&{rLUu_i+9z zPi`C%1v6Oeh!%fn`NVpgoJF<5o!OoC(O;(qu60lSb#+1L;()=I@T5?`0dr8p z3GTag?r!B}tP*@MQU3Fg%%Pznfz7;(nAcy*IvpDs7(Y?|nN>S8=6q?YjZs5Ldyh>z^?F}R`x6&^>=OsHsl#r+3IfNy;H6MQ8!N`L%E8T}Uc#)rSRgBkxFLLefD zEI3>Y@_6z<`MQ?)U&4-5SNZL=d%9GIg>LXST%QxJ{0y`MQwwVxDF`lX=nm zXd=h`&YssY6cwIGkFZ!Cj2X6VNXTi_i(B#1aQ8)Z3(au)i>f5w!s9RnV#ybO7eE%zTSjef> zg%95BNoXD>(wA-ijwxhnJzJ?ZD@6w03RWHqbnpg~_@8oF_l<}5a2sI|ABFR0_Z1vy zx?FwF-)*^7+p6a}{9ZNYfAxaTDZHo)ZCU^?coQ~7#%#|nGutdfE7%)5 zD{og!80u9sbU9>?+xNVZQ=tNB!WC#qAt z%tXWAbacDw!kCN`wr=)R2*z+H_1T|>4p}N}B&ae8BXN@&s!FR>ygKwLE=~}c1};H~ z*qF)0*q}4dxJhf(rDTUHio+#puVW>sV#dWVM$EjtDh7E`s2UNuzF~fD#-1^l=FrRc zm-HuQR4EqS;SRpo4{u}Hr)%`nYH5YWjgl%Jq88kz$J8F6di&1A3Tt;uzk7)7*>giC z|McjX#!YjTd5k*X4-XLEi~Ww_O;Rj7%v*NC`Bo zFyI}07!u`^cIG`Kv~RiEA1gK&PzsmeRk^od$sx)33~Z-sHHvf4%%xy&_AyoFDw9~{ zf@@u-$u3SAAN~?CD!Mf9k978tOh_M>Kh;mr>K>6F5z^EbHvD|1Mc@b~K1J%Hciti)nNClaBT)X{HF>3n z9$pgLHSfwOhmlP~7=mk&s-*d~ALU>NPA40nCq3BzIwZ@Y{A^q5%a2F>Jc0^4U#q%G zbe$X915Hb6M$+Z`znz+r=ebX^jFfIAUEesdQFAtbNTSN^OSf-zPyJTCj+KIR)u5(WW!_IpFIy}>qUQtI;iI0=-qq1+ZC4Ug8yI}j7;P>VFSw2!8>LquI{Tg z;#KC^kkq6%DVewQ;nHm;gPXFjeuFJs*a7@(s#SfPXnv1E)!Lx2zAE(v6?gp7%+05o0m@pvy+Ap0Rk%i z%$|74s^eKeLwz9CH@~(vId4VZL;D8#@G7pSGhlvvy?@7-VBqape1cXD;$lX{trIL( z%@^i`v`M`S7C$anIi!4f_ntk=PD@&vyW_))Y%4W$-J9l)m%C7jDCuq|Mp(YDu ze!;RiHDhy@8mOpG zs>s9yP}$k(l+8VJ!)CR_UY0J(!lva?#-B(0J$hwK#nfSdB%8J ze0&VMccZn;XrY#LJQ;Byv{j+NE;FaqM_2h?-Zag|3rMZ)m|hOr^(M@6r1ug_EgZFz z=py%kq1VgYRBK*+H(jMpFJVvC)LaBuY3aS`6p#8rlIdhOpltJ6O#fGUxZO)&pyzMW zq2tGVYnJq5U&W9^MeeMbqxJ3>sR2Bn;=X|)_wF)EM0L7hMdob2UiN*4GgZf? zW(%4*9^*hVDe~$FdTrR7F)w}zm1db~{snp`5y=Cb*_E4Gt8@b9<-D90x1Ba_1x1Em-I9PLDnH)?28~cQC1r<(+Z{T{3{J0znrk5m#z|=0aI0&D-zc?ckvEh!>Yz7w@k(dUeZ;Ww$wV0as7*84YS1Q^~J7TF^m}remU1UeWyGcF6UUNGuc@u zTCV85_(KG%%BTjjG%z*t3`@JWfE)Z9M0 zAARg{`K73LCzkTCT7!lIj*B+unFV29g%)7~NKZCbt=kJmQVE;MaT>iNq(Tx=Gzk_t zD{7)h)2F43p=HYWVD(U*KAG*(0N`1{uH+Fe)qJSZ6`6^9%TBWCi&&82)Z))E{P7I$ z?SNP8_9W~tr2wBc;L@U7`S!%$&Q?X-vKPN^3@WGhseX(S}x!WN?S z?Pe~Er})(?$x$Jxsg?yT6mJENW!ziiUgB{Q@f;L5M@$GA0QnsV8=|cghCbzM$7uZxo!zZem87Wl~~uS_PlO=bx8E z93g|dBy<-M3QISM$u3K`+xb8)nj=wIk{POwvbs3KcJs@r|bc|VRV`ij-bxm zESfu~FK#b_S<#wN_WmMGel_@4Dz(}N)G_8dk2u0yO(sypy1^=D)h9?K;m1nrcg*J{ zHTum9gp>V-)2_|HaItp>TC{rlAeDOKdf+U2mE90BYLY}63He(0qp$eifm(jU3&_YqC6lC81Q9zojHMwl*o`aWVvXsvw1{n#w4c%NA5<|uJwSh#320v z%Zz)V-}38|o^Uzk#uFxdlHay`GaIh?$Uw`RXPZP&^E}=4=GO`Z6&Bj<-NI`Z-v7D_ zyEVB+Iow+|jb2lRjFl?Q8I?5gY)xd&RoK!rrb%9FERDvGI23uZ!r1v%farH~enIox z>G=hz=E}JPkEo?$T%9(_@|4RZo*Jt8B;P=AaR0Q!xB1;pEiBZ3#({xpdA8iXgBF(! zF_MsymKgTs*IK*mXA<#Zn-)0JFI1tks6NI-UYRXtZ=4|r_&YQWfTI+P|J@9Pe*EBo zn39GKMZcK7wEcPLh~IwClE{hyXXj5uHVfPIQq!m66d)VfV1Zt#P(`V@di0L(=&0X@ zrMz`bmLrk@DFQ3;E#`EFKhnjxFHt&wN*qwXSm;kIqfffXffIi`$+>@A+rMDr1jii)xU zLX2bI#n&sj-q>`nfVa(v1KRiQ&fw^m+=R6CNY`x3)y0b0le}8WV(!-bWBy)ZwswKs z+HwX~zKYay;uHpB_Y%f?YL1^Z*DfS5GL$yB%w&725{SvDIwR-{LoJjG6c#S;- zF!DqqAdf{D2$3ehnR!;tEa7b+JBMFkn-fL!qSuH-nEv*;AT+{nxg|>ZXOr)2ZVDKj zgY+>mU_Mj&xZrbx$Y;f_~a%w#d!RLmj zXC96CzZp#bN``NX>Jf=WZ*Z1fB(%({tnQEk^cNQ7z~1H z>qZYgo+ZS}h_2`o25L_&*Wdj*?7lFsV3Tvf6U&S29X~&op@(wjooU)%7SCL)bsVlD zb$rXbNHjkpLd;O{$%FDTTH|>?4Y01}R6`YRxg6E*+G8)4=Rb55&vpq5w$K*NU4j;j zb)AEH_YO~5<;N!mBe8u$BKgWoi&ZRpj?XTFzBvF`zGqtmATgV(tD({P92>QO{4dR{ zX}=a~_4rF2UdV>GB99r)$r>Jo702Z~xRpENT1EuB1ey<-l}?KjXM9%&3bBViVt?r6^Eo-bBfxBAf?{;vF53oZ4bYU8#h&z8w@ zZmjwpp-;n%Sl5^;tV?i9rFRFMbZ+2>?PCvx&Ri%%_wG(Jq^yET(3iP2O60#Tu-5+m9s!a==af8YV{i~;2J zJ(DV2%W5ihb@^z^N)ZU&C5_H#OTm7om_&b;4g4_ub$yDFY-_w`4<_DR^zA$2Ly4&G z{N%(r54!kf;EVt_JtGsq$*I|Wb#P-SVa^-QewxY8<6Pxn7a!E^J9I;?e^}6GOjkbv zCEV2NAx?r5I;}|Z8-*X!erA#8B?Geoxn4?+a)` zG%IYw&qGPeBfzP=${_A5o51?p8t1l=UY@Qg3#}Mmnw)&|=rfUR)-1x&F$Gzy+UdF$ zmQj@vnTRodS0k#y7%ue4H4b)Hy6;d-w>S0zj+h}My3tna4_sA~{aAY9VW+z1$vb3< z3zj|_lQd2~6;yBNY>D&d$!@gF8y&2C1sBHFa1d zR0!|>^{S% zNqB|C(R=OsoL7uQ2@|8e8QKI};6Kpc4MR04t;X2wo`yHqw185tV)O@_1T7wAld`uO zUPDtqt8AZ+wOxLXs25{49X7*7mU@~shY-)_O!C5=5(ArQVIi5*4u<9JZ^#+wvBOKx z0S8Gv%^Aj9wvVYsQVz?&TKS7=!4d9C`MyEo3-N2?;q$8udJn1}41c7Q)4cXOMoQ`C zr?gTTblFdev&W16>tiACEw z)$F>bOBsAl`%FXow%}+KUEpn8-sh8s5oO3B3A9!zD;$XTp^s!{@oZp^#j{ip= z*xU6>#;Y6o&Ol0ql^AWya3e?v1C2z8zX;;P{Jha-KcZt0$eSLAsnJS8Im1@(i9V9ujr>6QG zHOi3=>mcs2S~Azmy$0hc8^TnN>=J8So(#=ocmtYmetJ1T^U}vXMxu8V=A@xXA5#MW zK52CIhnDbYzGdM3zl<)v$aGq>d}#&|Ve=b2nSZg~(qbp|&0yly9D3ftf>1g7OCZ$y zX0_{SAxOLkqCWDng@o1oDhW2cI?tr0)Y-(><}LfSsnOQJ)Gez|Tij5vE4czK5t_OC zIU_kPN(0V0(*aZ2`l=ich=A%#=v}MVA@a0J5v-PW@2-sqR|YVNAjv|Wy_Q!HWP_Nj zswCUxzMak+vYu?d(y^5!T;%hpwrgo;++*Sg%?# zRsJ|oIs=EVi|?CEKQZj++EA}lqw}lhohVFoGo8%A)|%R03i1%Q0``X9dR|Pszy@7~ z#Hsf0e$~f4E-46ch@>xNFdRsHY1iQDt*w!g>~NiXsODxN)JKY8pTUgy*yfys?BVLzlk0h1#HMpn!vE;UGVmpYHwvmctI~biFJ2?O~ z@pqbhqq5QqMY}XVdAST_g?jj&@&GkD8!e6KXuHtOWF3=srFv;8c2_q4zFQ;v9{t?c zBNZ$i*kkv27d3ep0G4y;dbA~4;#m>cG>Xt#0vH-&O^JvZ756S%Www&4K zyN4G6SdIG;ED=bQeq1{SloxIg&<2Pm8hIUKxR;<*<9|8k2%!21U|JbEMKefvv~Qt7 z&-)V&psgR_mPCYBRR2!Y0+yKr2qu$l z-U~AN_37TMjKiTWn1Fx9Q}a)}%(SfAay5-lY(|^ZdF>Ip*Zq<0tFcY31Ere-5y+myC83TjwV{v~c>Il1|7&mpBsg z&}*vIj5nLTL2jIiJjH!t7n^JP;$f%fb-B z{}K<2(h|-(pYE)H6b*ZDk#&M)z@dE1x+lq6?j;uiVSt6F_1TN&CR*SPY=vz-@g?kgv= zoFy9BBZN$RC-JHAKYesLE=3#sLB>m`^=4X%K39W0-c1+=wO6Pmsar9WhWR+7emQ5^ zSZNfqk{%sj;j8NdyE#WC$X&{X`|#$8ZAUtXXu?D8u1<%~>VRJdxy9hTAOVg2EQZ`o zO^wY?8#@Ypr}n5IOm0Py-=M1G)Dr1gF&phW^!JV5PnsZojuu5>y$~z3NXmMbGJ{gg zb_7=<9kMjNSLqwK4{VbRgY1@Dyz(L{SVPJh7ULW&6NO9?aVd4@IGK3DGLx4w+9AjA$+_KH|2RVkcsmKOcfi86j0A58 zB}qu%D0m&G%D}Cp1_B>#`ihzuRFYWfIa?({8hjzU+xD4Nli)&+;8dl{6bgF#I8u8A zeT_QRYfq$Z-IEgI9r7J)dO8#v|9-0a}Jo-E^_)FTTPOh{#L;bC*=Xv)X)O1Ze z>>C&1KPnXj-y^y!EB8+pj$H#7B3$ z;k49K2_HEv3f~{O5>a>V`<{rmO6d1{nSro ze%+)*WDTo`-HnSulW1FqvWkbB53vG{2M%Mn6|Jm$pSELc9Y=rI%zQi@C5z=(<{Dof z(^nMj!eTOh#YtANc}XOo2bNd+n&kUG@h?`H0{if@w;*xF5#<{T2nbIkC~T&$((0(& zm1Ijv`&auB8`hp)79b)RV|ThBtH4`h4BJSTIW`%UqeZ1mBJOd&m4|FR)e!8{s#|t@ z6HQ6&hs8Z>@`LE_6tYDzfm4p~$3}e5u#=1HbdD!|jN5*xt`XASqs-JClbf&Urhb-* zWs<5W)qL;H0?p0Ajjk%ns7F;Xs{~~kC^~?9wA@%3)&oc(Unz0Jj_B872qK}}B9XUyj>zywQda-r8X_9FsPJj}BmmWkWeRzrY z*KxCxdFKhHdb<5ZrpSuG*P);?>l|8$eBR^U%d(NyklfVw_aUJ;AA>U|>tFG49}ivV zWZBJQEE@~3Y?x#jTRs8H=F8kA`|x)x>%-|?a=pvLySHY?^e6SOHAy|lLo`QfV0T3@ zrtn;2lI6|9VP_bR2Gi%}Qn(PWt>^{v%R|9H%MZBF1n{l?{*C532UoN zSXX)WuX}92uCln!M0PrYKouyBVh5-hibiu=Ido8cQv0OA5Gx9tCtJ_Wo_bzPIOVW$uN5>q>q_wLCaurCHn?q!( zdVAo*w#R#^iBD+5>zW~M*C(jHZ*9!F!I!ssg{4A7#EbtgMFPcpoGuNVIFhw1^F%xrm@ufJT4!4xI{w!zf))Qbr_#b~}B%{HNdQW*o+i)V@QhJU_2naLIN3 zY_31yeSv3xnv2@%OWI-csOMD|z&<^>z?fKTJyBdD#?iLG;uyf@+E&d}CMc7qh82M? zRM@ph+e>5I<{{040>tR!tpx$V^~Q)QcOyWqn#eA0noV?EQSLlyPVU2)>xL0xc!*s| zHQUkv#iczE|0X-D`BVha(K}XV&c}B5JoQ>aoTqAum`hE-ag1$QwG}Nx%@ER`_Jim5ouIX zRV5Aa1^WpqO*kSQPAy4sbc6lbD^g%9KDN)6(~S6-zeYy@iQhp$mJmj~!T>%3|N6;~ zVd&SzW0v4EkAI*@!r%Y5>3;u4Zq;S}B}VQex-N6^4?FEmuyp^xU?9*bitx&sdfH01 z{!O%Lp!Y3+U{-$zrrfnt*F}+jl~Zm*YU{b8FhJG&CvpWzeh#`9tK%Q!YamEZT< zf&b=h2v1zm#S{Cm@#m${oJ11>`{D1?;Rdg9T%R0k(krcB=BQim0R+TNe9_e| zJ;Gz{BM(g-TY(BHQOfV)*dszI;3}qh$>wjJ2W5lh6JYNC&rMsHHpy;6ZR2TvN_Piv z*=OdLb;nKyQ*SoFNx-Xn%t_8{Gi*vZzWe@-Ft{n7>igwKW;Yb}$;h+CT44+I4>leWtV9E)>@C9Hnp|ei zM_e}ZeWn;woO3-dNxdLzh z6@j*4i6;6T8cFcw{_wRr64)JQS?J%s$ryz2UPv2YrmK;Zs-fRWlpuVuXszzPh3098 z%t&dCqs46~b<}nE`Y9m?e;)_%_6bw@dExp4=1b+#BD<1DlX)@d@uQkYm%(q?w_6GRsDz_(3_M7{EZ7pwFXXE6lW=Y(W01MO9H8?3OojH5xs9(ojEz^aso1Xy#5VdQQi! zZ@XS+I)Fsl9&QTqPWEeuE;oxEq9us;A4Ws2divo*J@l?0USGdhn?@59(B`O(i==U+ z=bha|9te!~nDhg>>X(h;)gfyyw^$;}Rz~CArG5)vj{=XXkWt zBWsFMB-li~P&H`&x=CCbee))R_Z+$4L3-IoY#51b!^Bx95>Sfn&;8M1roP#EerV)C zn&S83&-2V9M?1OlCZvjpZ!iQMXs`M9rB_zQTH|hKz$iCtP3dKLXZ;SsrBqER3!u)` z?;BPUPULoxD#5l&7lerpKD3+eOdcg9&4!QmVWEi)MxE>oe0u&MCIw**&qv5>imnue z2EG~Fe@%6}X*Ux)vBbJ_uxpK4JXfdVd-qKs=Q#b)R?QSo;$Cmw?E5v(3xQF+ch<&q zNp@xHewNYCIBieeSpN&l5E!M+Mobo(N~X>`f^M0U-1L^n2tOQ0rA=N9@~EbmqTH~A zx`MSejmBUJo6(Gwe1OlAVr_@6Twjp>PmI!c2Mgv3f1j z6tiKb{Mf^IvnZAQ)lFA63;03+C8#TV^l=93(j(Z{WeHw;qTilG6GrHB_rv`M-?m;g zT4d0hZCwl+^?kYztK}bZKe$+1BkC%r#B0`gbMDOhCXq-!>^+x}kNT@M5f1od=qTV; zzl?9PyemUBd?;mHO8fAzmtuAVm(=$hO&XeMyc(?62LkoMRWpBkBnk#+mxg@X-C|9$ zd##@#;h1hR+1B8#nmxHJWAIqg^9UYHNa_#pfPL!@mtyqP!&h7qf1FNI>m!z0QOEfW z7Dqu%=a8x1i{Yv1hZPbfBa#teHOI{Y{yg1&8}9HmS?BxQrAj&Bt7N3j+)XrNhjZrM zmisv}p6y*S=xI8HKZ#H4lc;uu19&~!aUMQ5*hbe!M)m?4xG%hO8qK}?Zei}_^Ou=m zBmwtIWhS=~WEnk4BC9HI$paKj>=NkxUJvbrWP)MsYkeVo4|q>l5Mv<%)@o$9bSym$ z<|3~NxWb3uH>!pgoJNt8P8XIvym(pmOYuQj)=a;u?Y%jD?VEku(L1)Ihvp1pE{v-l zfP)IcJh?|cOt*G2B$Iv_%G7=dGmsvZ5mVGuI8zT}Ks1aP%EL0lrJu?4>jZa5gF~y7Vbu(JZF%wB4r?VBO!v?|aKkWnKiDM)|MPmA;xswaJKMkvf}rvu zRc&i+GXnx6#pL~fkq=!JYBRhsI`y>Qv#SoL#S)k)wrCyHH0H}bNI?(D#83xG?=_|= z%xrll`tuLgx6AGWS^_{r7cU*TzWR|R53lAi%fMT3M$->cce8S9H%1^GtaMttkTH?e zPXlOnTWN{D?me?L$JI%R!9%TUIQ!7QWsm z7cGY^XuG`Tk=S#|%cZR9^6zISM!N)NP!NVclpmT&=dfJ9hfDJfUaq|3-jBJf1}PB( z9nS8GjeXRGjiA?%+uy=PzN`on>u!4Ue4Lq@Q=IN}f)oXQDMo3i1-oNS1!Wix(M*&2 zLZ5Yx@IXZV5gr)fCKC~+_L~UYy))}3<9|87W`uBYG@HxjDO?H$!OZLXz1%3LWo~$3 z&Jyy`C}4i8ZI%vmdA%&{tqS@hFwok$JsC_1T?qfs9mKxNM5=3119o)tm1t_HlW5e+ zzz+Ds1=W<^V>7%|$4 zsj86y@v(PHh%gOj|4c)t@6N!+^Tnl3`>eyrWQ56G61%HaHx!y07X7wPcICE^UBaJ} zg=DyA4DFO%&lv49*{0+7g2^E9N$s^g^~0mn=jNN^F|YmRDW<)TmY~_)V>@Bu#Va#t z^6tS<{Fve)y)dkjsl#16s-p`sb_e6K19kN5`Z{xVcKH+f$2X)TZy6l`3cil69e;p= zj}+fDA{FPy>elel1)|?P2E9xh6o6uL>A5F-b%i=;xq@LUM=rxldN{apBs@AIcfq?J zcDq*UIz0Jv+bc?)>gltJMKm{PBhkw)fjEEwiFWS-RvN;D%hrc2=J4K!+D6}*A-qY| zj9b4540%2(;P2qD&}0#f=B-;|5aIG&GR9OAzFd|nZr8RX7Llhe$wMZMe9vJp1&?>Z7Lsq6bIaX3=y^avHR@5=8wp0;!-;GASZBjpEmOf z@zK)x4JAi-!KaAJWzX{mWpD=0h=KO&TTDnEuGpA>(?p#QG`a5gtpP}6#RSzH%SgR04|u!rq`PHA7S19i}- z8}dR9=JQF%%Y%Tvog{Z-+cVFPQ_kZEmHYl-@+3Z}GHH5-|7%MUPhUy~_V1vc`uTK! zg_+eFCbNcr_rQ&EP)q;wIqRp-tp6MDb$kr$m)I^tZt$q%RiO7f2Wx2n#30`H(pL&aXhGhrQGe_U){36 zV15z)Bj(BU`?h-df8LlsZCn4}!fFrGx#oZCzI;xzhNJ&9~8g5NUy1Pi{x|9T{a*mZ3zWB>Hqxw z|56K1>ux{cYkpVd_V)baVoQL%HAO~nFJkWgxTi5J(l4E0y(XC^t^e^%RnZLF?EV|} zT0H~0^nk&K>uUo`bqzfJ-H*QVzAyk7NJp-P@jFiaug`qM{mMqrg2Fb>5Lxb=B`fYf zQ~-CsELs-KTd`kZ!&evCXrOe$4A-k$4=A|)eP_l5>dpbXtia$0f?vQDYZPq_AhLT; z{ZWKn0*Wu9?|qtfA{99E0Zd8{7d(Fl%zsI*omv(E@2T~EvWXESexTq&aXC1FTm()P z#{aLXY6cIn7#!IXx1fs8AL#vQW=1TbzwFOvuiqQCZs#*8U}Uo=Le}XyJDtDyH754_ z&98TA1sGQVvzS4N9lH!vFx-f#>Rb2xRU88_Wh-nmvtkLo z_upe{_TSR$v7jC0z$j|@E_1<=bphy~{KIqL9u=VY1Yl_Z z0v+~1M>_*|cY%34z;0&4LQwkw1lWK@!+{IH4kMV?3_8x`1-P{h3Hx?+p00i_ I>zopr0J!dQ2mk;8 literal 42136 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 gEy=ezc%8s{0tr=TYZh7LFW2U>e!ZvX%Q diff --git a/src/EMSESPDevicesService.cpp b/src/EMSESPDevicesService.cpp index 1ce990286..585b2b4c8 100644 --- a/src/EMSESPDevicesService.cpp +++ b/src/EMSESPDevicesService.cpp @@ -4,22 +4,37 @@ namespace emsesp { -EMSESPDevicesService::EMSESPDevicesService(AsyncWebServer * server, SecurityManager * securityManager) { +EMSESPDevicesService::EMSESPDevicesService(AsyncWebServer * server, SecurityManager * securityManager) + : _timeHandler(DEVICE_DATA_SERVICE_PATH, + securityManager->wrapCallback(std::bind(&EMSESPDevicesService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED)) { + // body server->on(EMSESP_DEVICES_SERVICE_PATH, HTTP_GET, - securityManager->wrapRequest(std::bind(&EMSESPDevicesService::emsespDevicesService, this, std::placeholders::_1), - AuthenticationPredicates::IS_AUTHENTICATED)); + securityManager->wrapRequest(std::bind(&EMSESPDevicesService::all_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED)); + + server->on(SCAN_DEVICES_SERVICE_PATH, + HTTP_GET, + securityManager->wrapRequest(std::bind(&EMSESPDevicesService::scan_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED)); + + _timeHandler.setMethod(HTTP_POST); + _timeHandler.setMaxContentLength(256); + server->addHandler(&_timeHandler); } -void EMSESPDevicesService::emsespDevicesService(AsyncWebServerRequest * request) { +void EMSESPDevicesService::scan_devices(AsyncWebServerRequest * request) { + EMSESP::send_read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER); + request->send(200); +} + +void EMSESPDevicesService::all_devices(AsyncWebServerRequest * request) { AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_STATUS_SIZE); JsonObject root = response->getRoot(); JsonArray devices = root.createNestedArray("devices"); - for (const auto & emsdevice : EMSESP::emsdevices) { if (emsdevice) { JsonObject deviceRoot = devices.createNestedObject(); + deviceRoot["id"] = emsdevice->unique_id(); deviceRoot["type"] = emsdevice->device_type_name(); deviceRoot["brand"] = emsdevice->brand_to_string(); deviceRoot["name"] = emsdevice->name(); @@ -28,8 +43,23 @@ void EMSESPDevicesService::emsespDevicesService(AsyncWebServerRequest * request) deviceRoot["version"] = emsdevice->version(); } } + response->setLength(); request->send(response); } +void EMSESPDevicesService::device_data(AsyncWebServerRequest * request, JsonVariant & json) { + if (json.is()) { + uint8_t id = json["id"]; // get id from selected table row + + AsyncJsonResponse * response = new AsyncJsonResponse(false, 1024); + EMSESP::device_info(id, (JsonObject &)response->getRoot()); + response->setLength(); + request->send(response); + } else { + AsyncWebServerResponse * response = request->beginResponse(200); + request->send(response); + } +} + } // namespace emsesp \ No newline at end of file diff --git a/src/EMSESPDevicesService.h b/src/EMSESPDevicesService.h index 02f7187f1..db242984e 100644 --- a/src/EMSESPDevicesService.h +++ b/src/EMSESPDevicesService.h @@ -6,23 +6,26 @@ #include #include -// #include -// #include -// #include - -#include "version.h" - #define MAX_EMSESP_STATUS_SIZE 1024 -#define EMSESP_DEVICES_SERVICE_PATH "/rest/emsespDevices" + +#define EMSESP_DEVICES_SERVICE_PATH "/rest/allDevices" +#define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices" +#define DEVICE_DATA_SERVICE_PATH "/rest/deviceData" namespace emsesp { +using namespace std::placeholders; // for `_1` + class EMSESPDevicesService { public: EMSESPDevicesService(AsyncWebServer * server, SecurityManager * securityManager); private: - void emsespDevicesService(AsyncWebServerRequest * request); + void all_devices(AsyncWebServerRequest * request); + void scan_devices(AsyncWebServerRequest * request); + + AsyncCallbackJsonWebHandler _timeHandler; + void device_data(AsyncWebServerRequest * request, JsonVariant & json); }; } // namespace emsesp diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 44b72d7ba..637c60bba 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -166,6 +166,24 @@ void Boiler::boiler_cmd_wwtemp(const char * message) { } } +void Boiler::device_info(JsonArray & root) { + JsonObject dataElement; + + dataElement = root.createNestedObject(); + dataElement["name"] = F("Hot tap water"); + dataElement["value"] = tap_water_active_ ? F("running") : F("off"); + + dataElement = root.createNestedObject(); + dataElement["name"] = F("Central heating"); + dataElement["value"] = heating_active_ ? F("active") : F("off"); + + render_value_json(root, "", F("Selected flow temperature"), selFlowTemp_, F_(degrees)); + render_value_json(root, "", F("Current flow temperature"), curFlowTemp_, F_(degrees), 10); + render_value_json(root, "", F("Warm Water selected temperature"), wWSelTemp_, F_(degrees)); + render_value_json(root, "", F("Warm Water set temperature"), wWSetTmp_, F_(degrees)); + render_value_json(root, "", F("Warm Water current temperature (intern)"), wWCurTmp_, F_(degrees), 10); +} + // publish values via MQTT void Boiler::publish_values() { const size_t capacity = JSON_OBJECT_SIZE(47); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/ diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 41f7e8160..60e4170e1 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -40,6 +40,7 @@ class Boiler : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/connect.cpp b/src/devices/connect.cpp index 07fb346c7..d8da0d612 100644 --- a/src/devices/connect.cpp +++ b/src/devices/connect.cpp @@ -34,6 +34,9 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con // register_mqtt_topic("topic", std::bind(&Connect::cmd, this, _1)); } +void Connect::device_info(JsonArray & root) { +} + void Connect::add_context_menu() { } diff --git a/src/devices/connect.h b/src/devices/connect.h index 37bf8a7e9..51956ef5a 100644 --- a/src/devices/connect.h +++ b/src/devices/connect.h @@ -37,6 +37,7 @@ class Connect : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/controller.cpp b/src/devices/controller.cpp index c7d5727e9..a6796ce8b 100644 --- a/src/devices/controller.cpp +++ b/src/devices/controller.cpp @@ -39,6 +39,9 @@ Controller::Controller(uint8_t device_type, uint8_t device_id, uint8_t product_i void Controller::add_context_menu() { } +void Controller::device_info(JsonArray & root) { +} + // display all values into the shell console void Controller::show_values(uuid::console::Shell & shell) { EMSdevice::show_values(shell); // always call this to show header diff --git a/src/devices/controller.h b/src/devices/controller.h index 36866a8f7..d905b3864 100644 --- a/src/devices/controller.h +++ b/src/devices/controller.h @@ -37,6 +37,7 @@ class Controller : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/gateway.cpp b/src/devices/gateway.cpp index 101791f1d..aa8bec0bf 100644 --- a/src/devices/gateway.cpp +++ b/src/devices/gateway.cpp @@ -39,6 +39,9 @@ Gateway::Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, con void Gateway::add_context_menu() { } +void Gateway::device_info(JsonArray & root) { +} + // display all values into the shell console void Gateway::show_values(uuid::console::Shell & shell) { EMSdevice::show_values(shell); // always call this to show header diff --git a/src/devices/gateway.h b/src/devices/gateway.h index e27eff894..d38dbb32e 100644 --- a/src/devices/gateway.h +++ b/src/devices/gateway.h @@ -37,6 +37,7 @@ class Gateway : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/heatpump.cpp b/src/devices/heatpump.cpp index fec97a61d..14b8e74ea 100644 --- a/src/devices/heatpump.cpp +++ b/src/devices/heatpump.cpp @@ -50,6 +50,9 @@ Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, c void Heatpump::add_context_menu() { } +void Heatpump::device_info(JsonArray & root) { +} + // display all values into the shell console void Heatpump::show_values(uuid::console::Shell & shell) { EMSdevice::show_values(shell); // always call this to show header diff --git a/src/devices/heatpump.h b/src/devices/heatpump.h index dda5acd3c..a23921698 100644 --- a/src/devices/heatpump.h +++ b/src/devices/heatpump.h @@ -37,6 +37,7 @@ class Heatpump : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/mixing.cpp b/src/devices/mixing.cpp index 52938aff7..c4ebe6ced 100644 --- a/src/devices/mixing.cpp +++ b/src/devices/mixing.cpp @@ -57,6 +57,9 @@ Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const void Mixing::add_context_menu() { } +void Mixing::device_info(JsonArray & root) { +} + // check to see if values have been updated bool Mixing::updated_values() { return false; diff --git a/src/devices/mixing.h b/src/devices/mixing.h index ed1e8ce39..d1c6b09b9 100644 --- a/src/devices/mixing.h +++ b/src/devices/mixing.h @@ -37,6 +37,7 @@ class Mixing : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index feb2987c0..d3e9e0c09 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -58,6 +58,9 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s void Solar::add_context_menu() { } +void Solar::device_info(JsonArray & root) { +} + // display all values into the shell console void Solar::show_values(uuid::console::Shell & shell) { EMSdevice::show_values(shell); // always call this to show header diff --git a/src/devices/solar.h b/src/devices/solar.h index 5a1be5e5b..55b3e398a 100644 --- a/src/devices/solar.h +++ b/src/devices/solar.h @@ -37,6 +37,7 @@ class Solar : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/switch.cpp b/src/devices/switch.cpp index f86858fff..c7a160260 100644 --- a/src/devices/switch.cpp +++ b/src/devices/switch.cpp @@ -39,6 +39,9 @@ Switch::Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const void Switch::add_context_menu() { } +void Switch::device_info(JsonArray & root) { +} + // display all values into the shell console void Switch::show_values(uuid::console::Shell & shell) { EMSdevice::show_values(shell); // always call this to show header diff --git a/src/devices/switch.h b/src/devices/switch.h index cf74014eb..44d9844d6 100644 --- a/src/devices/switch.h +++ b/src/devices/switch.h @@ -37,6 +37,7 @@ class Switch : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index 9c52e0760..2978bab35 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -169,6 +169,50 @@ void Thermostat::init_mqtt() { register_mqtt_topic("thermostat_cmd_mode", std::bind(&Thermostat::thermostat_cmd_mode, this, _1)); } +// prepare data for Web UI +void Thermostat::device_info(JsonArray & root) { + JsonObject dataElement; + + uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, strip the option bits + + for (const auto & hc : heating_circuits_) { + if (!Helpers::hasValue(hc->setpoint_roomTemp)) { + break; // skip this HC + } + + // different thermostat types store their temperature values differently + uint8_t format_setpoint, format_curr; + switch (flags) { + case EMS_DEVICE_FLAG_EASY: + format_setpoint = 100; // *100 + format_curr = 100; // *100 + break; + case EMS_DEVICE_FLAG_JUNKERS: + format_setpoint = 10; // *10 + format_curr = 10; // *10 + break; + default: // RC30, RC35 etc... + format_setpoint = 2; // *2 + format_curr = 10; // *10 + break; + } + + // create prefix with heating circuit number + std::string hc_str(5, '\0'); + snprintf_P(&hc_str[0], hc_str.capacity() + 1, PSTR("hc%d: "), hc->hc_num()); + + render_value_json(root, hc_str, F("Current room temperature"), hc->curr_roomTemp, F_(degrees), format_curr); + render_value_json(root, hc_str, F("Setpoint room temperature"), hc->setpoint_roomTemp, F_(degrees), format_setpoint); + if (Helpers::hasValue(hc->mode)) { + dataElement = root.createNestedObject(); + std::string mode_str(15, '\0'); + snprintf_P(&mode_str[0], mode_str.capacity() + 1, PSTR("%sMode"), hc_str.c_str()); + dataElement["name"] = mode_str; + dataElement["value"] = mode_tostring(hc->get_mode(flags)); + } + } +} + // only add the menu for the master thermostat void Thermostat::add_context_menu() { if (device_id() != EMSESP::actual_master_thermostat()) { @@ -1024,8 +1068,6 @@ void Thermostat::show_values(uuid::console::Shell & shell) { } } - // std::sort(heating_circuits_.begin(), heating_circuits_.end()); // sort based on hc number. This has moved to the heating_circuit() function - for (const auto & hc : heating_circuits_) { if (!Helpers::hasValue(hc->setpoint_roomTemp)) { break; // skip this HC diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index 8f2ea0c76..ee019b87b 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -94,6 +94,7 @@ class Thermostat : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); + virtual void device_info(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 16341f8ad..2185b17df 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -127,7 +127,7 @@ uint8_t EMSdevice::decode_brand(uint8_t value) { } } -// print human friendly description of the EMS device +// returns string of a human friendly description of the EMS device std::string EMSdevice::to_string() const { std::string str(160, '\0'); @@ -153,6 +153,17 @@ std::string EMSdevice::to_string() const { return str; } +// returns out brand + device name +std::string EMSdevice::to_string_short() const { + std::string str(160, '\0'); + if (brand_ == Brand::NO_BRAND) { + snprintf_P(&str[0], str.capacity() + 1, PSTR("%s: %s"), device_type_name().c_str(), name_.c_str()); + } else { + snprintf_P(&str[0], str.capacity() + 1, PSTR("%s: %s %s"), device_type_name().c_str(), brand_to_string().c_str(), name_.c_str()); + } + return str; +} + // prints the header for the section void EMSdevice::show_values(uuid::console::Shell & shell) { shell.printfln(F("%s: %s"), device_type_name().c_str(), to_string().c_str()); diff --git a/src/emsdevice.h b/src/emsdevice.h index 5030e1108..2abc3bc42 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -102,12 +102,22 @@ class EMSdevice { return name_; } + inline uint8_t unique_id() const { + return unique_id_; + } + + inline void unique_id(uint8_t unique_id) { + unique_id_ = unique_id; + } + std::string brand_to_string() const; static uint8_t decode_brand(uint8_t value); std::string to_string() const; - void show_telegram_handlers(uuid::console::Shell & shell); - void show_mqtt_handlers(uuid::console::Shell & shell); + std::string to_string_short() const; + + void show_telegram_handlers(uuid::console::Shell & shell); + void show_mqtt_handlers(uuid::console::Shell & shell); using process_function_p = std::function)>; void register_telegram_type(const uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, process_function_p cb); @@ -126,6 +136,7 @@ class EMSdevice { virtual void publish_values() = 0; virtual bool updated_values() = 0; virtual void add_context_menu() = 0; + virtual void device_info(JsonArray & root) = 0; std::string telegram_type_name(std::shared_ptr telegram); @@ -165,6 +176,37 @@ class EMSdevice { } } + // takes a value from an ems device and creates a nested json (name, value) + // which can be passed to the web UI + template + static void render_value_json(JsonArray & json, + const std::string & prefix, + const __FlashStringHelper * name, + Value & value, + const __FlashStringHelper * suffix, + const uint8_t format = 0) { + char buffer[15]; + if (Helpers::render_value(buffer, value, format) == nullptr) { + return; + } + + JsonObject dataElement = json.createNestedObject(); + + // copy flash into std::strings to ensure arduinojson can reference them without a copy + + if (suffix != nullptr) { + std::string text(20, '\0'); + snprintf_P(&text[0], text.capacity() + 1, PSTR("%s%s"), buffer, uuid::read_flash_string(suffix).c_str()); + dataElement["value"] = text; + } else { + dataElement["value"] = buffer; + } + + std::string text2(100, '\0'); + snprintf_P(&text2[0], text2.capacity() + 1, PSTR("%s%s"), prefix.c_str(), uuid::read_flash_string(name).c_str()); + dataElement["name"] = text2; + } + static void print_value(uuid::console::Shell & shell, uint8_t padding, const __FlashStringHelper * name, const __FlashStringHelper * value); static void print_value(uuid::console::Shell & shell, uint8_t padding, const __FlashStringHelper * name, const char * value); @@ -229,6 +271,7 @@ class EMSdevice { static constexpr uint8_t EMS_DEVICE_FLAG_JUNKERS_2 = (1 << 6); // 6th bit set if older models private: + uint8_t unique_id_; uint8_t device_type_ = DeviceType::UNKNOWN; uint8_t device_id_ = 0; uint8_t product_id_ = 0; diff --git a/src/emsesp.cpp b/src/emsesp.cpp index a927dc573..b6e2b6e72 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -39,9 +39,8 @@ ESP8266React EMSESP::esp8266React(&webServer, &dummyFS); EMSESPSettingsService EMSESP::emsespSettingsService = EMSESPSettingsService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager()); #endif -EMSESPStatusService EMSESP::emsespStatusService = EMSESPStatusService(&webServer, EMSESP::esp8266React.getSecurityManager()); -EMSESPDevicesService EMSESP::emsespDevicesService = EMSESPDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager()); -EMSESPScanDevicesService EMSESP::emsespScanDevicesService = EMSESPScanDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager()); +EMSESPStatusService EMSESP::emsespStatusService = EMSESPStatusService(&webServer, EMSESP::esp8266React.getSecurityManager()); +EMSESPDevicesService EMSESP::emsespDevicesService = EMSESPDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager()); std::vector> EMSESP::emsdevices; // array of all the detected EMS devices std::vector EMSESP::device_library_; // libary of all our known EMS devices so far @@ -63,6 +62,7 @@ uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; / uint8_t EMSESP::watch_ = 0; // trace off bool EMSESP::tap_water_active_ = false; // for when Boiler states we having running warm water. used in Shower() uint32_t EMSESP::last_fetch_ = 0; +uint8_t EMSESP::unique_id_count_ = 0; // for a specific EMS device go and request data values // or if device_id is 0 it will fetch from all our known and active devices @@ -474,6 +474,20 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { return found; } +// calls the device handler's function to populate a json doc with device info +void EMSESP::device_info(const uint8_t unique_id, JsonObject & root) { + for (const auto & emsdevice : emsdevices) { + if (emsdevice) { + if (emsdevice->unique_id() == unique_id) { + root["deviceName"] = emsdevice->to_string_short(); // can;t use c_str() because of scope + JsonArray data = root.createNestedArray("deviceData"); + emsdevice->device_info(data); + return; + } + } + } +} + // return true if we have this device already registered bool EMSESP::device_exists(const uint8_t device_id) { for (const auto & emsdevice : emsdevices) { @@ -513,6 +527,9 @@ void EMSESP::show_devices(uuid::console::Shell & shell) { // shell.printf(F("[factory ID: %d] "), device_class.first); for (const auto & emsdevice : emsdevices) { if ((emsdevice) && (emsdevice->device_type() == device_class.first)) { +#if defined(EMSESP_DEBUG) + shell.printf(F("[id=%d] "), emsdevice->unique_id()); +#endif shell.printf(F("%s: %s"), emsdevice->device_type_name().c_str(), emsdevice->to_string().c_str()); if ((emsdevice->device_type() == EMSdevice::DeviceType::THERMOSTAT) && (emsdevice->device_id() == actual_master_thermostat())) { shell.printf(F(" ** master device **")); @@ -584,6 +601,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std:: } else { emsdevices.push_back( EMSFactory::add(device_p->device_type, device_id, device_p->product_id, version, uuid::read_flash_string(device_p->name), device_p->flags, brand)); + emsdevices.back()->unique_id(++unique_id_count_); LOG_DEBUG(F("Adding new device with device ID 0x%02X with product ID %d and version %s"), device_id, product_id, version.c_str()); // go and fetch its data, fetch_device_values(device_id); @@ -745,11 +763,11 @@ void EMSESP::loop() { } system_.loop(); // does LED and checks system health, and syslog service - mqtt_.loop(); // starts mqtt, and sends out anything in the queue rxservice_.loop(); // process what ever is in the rx queue txservice_.loop(); // check that the Tx is all ok shower_.loop(); // check for shower on/off sensors_.loop(); // this will also send out via MQTT + mqtt_.loop(); // sends out anything in the queue via MQTT console_.loop(); // telnet/serial console // force a query on the EMS devices to fetch latest data at a set interval (1 min) diff --git a/src/emsesp.h b/src/emsesp.h index 9829c002a..28d62c860 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -37,7 +37,6 @@ #include "EMSESPStatusService.h" #include "EMSESPDevicesService.h" #include "EMSESPSettingsService.h" -#include "EMSESPScanDevicesService.h" #include "emsdevice.h" #include "emsfactory.h" @@ -82,6 +81,8 @@ class EMSESP { static void send_raw_telegram(const char * data); static bool device_exists(const uint8_t device_id); + static void device_info(const uint8_t unique_id, JsonObject & root); + static uint8_t count_devices(const uint8_t device_type); static uint8_t actual_master_thermostat(); @@ -146,11 +147,10 @@ class EMSESP { static TxService txservice_; // web controllers - static ESP8266React esp8266React; - static EMSESPSettingsService emsespSettingsService; - static EMSESPStatusService emsespStatusService; - static EMSESPDevicesService emsespDevicesService; - static EMSESPScanDevicesService emsespScanDevicesService; + static ESP8266React esp8266React; + static EMSESPSettingsService emsespSettingsService; + static EMSESPStatusService emsespStatusService; + static EMSESPDevicesService emsespDevicesService; static uuid::log::Logger logger() { return logger_; @@ -182,6 +182,8 @@ class EMSESP { static uint16_t watch_id_; static uint8_t watch_; static bool tap_water_active_; + + static uint8_t unique_id_count_; }; } // namespace emsesp diff --git a/src/emsfactory.h b/src/emsfactory.h index ede08d8fe..50b2d0afe 100644 --- a/src/emsfactory.h +++ b/src/emsfactory.h @@ -89,6 +89,7 @@ class ConcreteEMSFactory : EMSFactory { ConcreteEMSFactory(const uint8_t device_type) { EMSFactory::registerFactory(device_type, this); } + auto construct(uint8_t device_type, uint8_t device_id, uint8_t product_id, std::string version, std::string name, uint8_t flags, uint8_t brand) const -> EMSdevice * { return new DerivedClass(device_type, device_id, product_id, version, name, flags, brand); From 1a2e405ffedb39d6112f02b1d8e0f1e55e32d649 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 16:03:52 +0200 Subject: [PATCH 16/66] do not reset wifi when value changed (in progress) - #435 --- src/system.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index efdafe763..854a9edc2 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -558,15 +558,11 @@ void System::console_commands(Shell & shell, unsigned int context) { flash_string_vector{F_(set), F_(wifi), F_(ssid)}, flash_string_vector{F_(name_mandatory)}, [](Shell & shell, const std::vector & arguments) { - shell.println("The wifi connection will be reset..."); - Shell::loop_all(); - delay(1000); // wait a second - EMSESP::esp8266React.getWiFiSettingsService()->update( - [&](WiFiSettings & wifiSettings) { - wifiSettings.ssid = arguments.front().c_str(); - return StateUpdateResult::CHANGED; - }, - "local"); + EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation([&](WiFiSettings & wifiSettings) { + wifiSettings.ssid = arguments.front().c_str(); + return StateUpdateResult::CHANGED; + }); + shell.println("You will need to restart to apply the new WiFi changes"); }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, @@ -579,13 +575,12 @@ void System::console_commands(Shell & shell, unsigned int context) { [password1](Shell & shell, bool completed, const std::string & password2) { if (completed) { if (password1 == password2) { - EMSESP::esp8266React.getWiFiSettingsService()->update( + EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation( [&](WiFiSettings & wifiSettings) { wifiSettings.password = password2.c_str(); return StateUpdateResult::CHANGED; - }, - "local"); - shell.println(F("WiFi password updated")); + }); + shell.println("You will need to restart to apply the new WiFi changes"); } else { shell.println(F("Passwords do not match")); } From ce30346ac3245256452f99b00620ed7f20c427eb Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 16:04:17 +0200 Subject: [PATCH 17/66] prevent forcing DEBUG logging on an ESP32 --- src/console.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/console.cpp b/src/console.cpp index fe7020ff1..e5cf869de 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -516,7 +516,7 @@ void Console::start() { shell = std::make_shared(serial_console_, true); shell->maximum_log_messages(100); // default is 50 shell->start(); - shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all + // shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all #endif #if defined(EMSESP_STANDALONE) From 90d33259f23a35090996006f61132fc10b96a6c5 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 20:29:30 +0200 Subject: [PATCH 18/66] tidy up how EMS bus line state is handled --- interface/src/project/EMSESPDevicesForm.tsx | 19 +++-------- src/emsesp.cpp | 35 ++++++++++++++------- src/telegram.cpp | 22 +++---------- src/telegram.h | 25 +++------------ 4 files changed, 38 insertions(+), 63 deletions(-) diff --git a/interface/src/project/EMSESPDevicesForm.tsx b/interface/src/project/EMSESPDevicesForm.tsx index 2f6c2b83a..d90904aba 100644 --- a/interface/src/project/EMSESPDevicesForm.tsx +++ b/interface/src/project/EMSESPDevicesForm.tsx @@ -123,7 +123,7 @@ class EMSESPDevicesForm extends Component - No EMS devices found. Check the connection and for possible Tx errors and try scanning for new devices. + No EMS devices found. Check the connections and for possible Tx errors. ) @@ -205,24 +205,15 @@ class EMSESPDevicesForm extends Component - ) + return; } - if (!deviceData) { - return ( - -

- Click on a device to show it's values - - ); + if (!deviceData) { + return; } if ((deviceData.deviceData || []).length === 0) { - return ( -

- ); + return; } return ( diff --git a/src/emsesp.cpp b/src/emsesp.cpp index b6e2b6e72..5cc61a255 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -151,7 +151,22 @@ uint8_t EMSESP::bus_status() { // show the EMS bus status plus both Rx and Tx queues void EMSESP::show_ems(uuid::console::Shell & shell) { // EMS bus information - if (rxservice_.bus_connected()) { + switch (bus_status()) { + case BUS_STATUS_OFFLINE: + shell.printfln(F("EMS Bus is disconnected.")); + break; + case BUS_STATUS_TX_ERRORS: + shell.printfln(F("EMS Bus is connected, but Tx is not stable.")); + break; + case BUS_STATUS_CONNECTED: + default: + shell.printfln(F("EMS Bus is connected.")); + break; + } + + shell.println(); + + if (bus_status() != BUS_STATUS_OFFLINE) { uint8_t success_rate = 0; if (rxservice_.telegram_error_count()) { success_rate = ((float)rxservice_.telegram_error_count() / (float)rxservice_.telegram_count()) * 100; @@ -165,8 +180,6 @@ void EMSESP::show_ems(uuid::console::Shell & shell) { shell.printfln(F(" #write requests sent: %d"), txservice_.telegram_write_count()); shell.printfln(F(" #corrupted telegrams: %d (%d%%)"), rxservice_.telegram_error_count(), success_rate); shell.printfln(F(" #tx fails (after %d retries): %d"), TxService::MAXIMUM_TX_RETRIES, txservice_.telegram_fail_count()); - } else { - shell.printfln(F("EMS Bus is disconnected.")); } shell.println(); @@ -647,14 +660,14 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { } // are we waiting for a response from a recent Tx Read or Write? - uint8_t op = EMSbus::tx_waiting(); - if (op != Telegram::Operation::NONE) { + uint8_t tx_state = EMSbus::tx_state(); + if (tx_state != Telegram::Operation::NONE) { bool tx_successful = false; - EMSbus::tx_waiting(Telegram::Operation::NONE); // reset Tx wait state + EMSbus::tx_state(Telegram::Operation::NONE); // reset Tx wait state // txservice_.print_last_tx(); // if we're waiting on a Write operation, we want a single byte 1 or 4 - if ((op == Telegram::Operation::TX_WRITE) && (length == 1)) { + if ((tx_state == Telegram::Operation::TX_WRITE) && (length == 1)) { if (first_value == TxService::TX_WRITE_SUCCESS) { LOG_DEBUG(F("Last Tx write successful")); txservice_.increment_telegram_write_count(); // last tx/write was confirmed ok @@ -667,7 +680,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { txservice_.send_poll(); // close the bus txservice_.reset_retry_count(); } - } else if (op == Telegram::Operation::TX_READ) { + } else if (tx_state == Telegram::Operation::TX_READ) { // got a telegram with data in it. See if the src/dest matches that from the last one we sent and continue to process it uint8_t src = data[0]; uint8_t dest = data[1]; @@ -682,13 +695,15 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { // if Tx wasn't successful, retry or just give up if (!tx_successful) { - txservice_.retry_tx(op, data, length); + txservice_.retry_tx(tx_state, data, length); return; } } // check for poll if (length == 1) { + EMSbus::last_bus_activity(uuid::get_uptime()); // set the flag indication the EMS bus is active + #ifdef EMSESP_DEBUG char s[4]; if (first_value & 0x80) { @@ -702,7 +717,6 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { // check for poll to us, if so send top message from Tx queue immediately and quit // if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80) if ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id()) { - EMSbus::last_bus_activity(uuid::get_uptime()); // set the flag indication the EMS bus is active txservice_.send(); } // send remote room temperature if active @@ -764,7 +778,6 @@ void EMSESP::loop() { system_.loop(); // does LED and checks system health, and syslog service rxservice_.loop(); // process what ever is in the rx queue - txservice_.loop(); // check that the Tx is all ok shower_.loop(); // check for shower on/off sensors_.loop(); // this will also send out via MQTT mqtt_.loop(); // sends out anything in the queue via MQTT diff --git a/src/telegram.cpp b/src/telegram.cpp index 22ebe694b..8fa73d8e6 100644 --- a/src/telegram.cpp +++ b/src/telegram.cpp @@ -42,8 +42,7 @@ uint32_t EMSbus::last_bus_activity_ = 0; // timestamp of last time bool EMSbus::bus_connected_ = false; // start assuming the bus hasn't been connected uint8_t EMSbus::ems_mask_ = EMS_MASK_UNSET; // unset so its triggered when booting, the its 0x00=buderus, 0x80=junker/ht3 uint8_t EMSbus::ems_bus_id_ = EMSESP_DEFAULT_EMS_BUS_ID; -uint8_t EMSbus::tx_waiting_ = Telegram::Operation::NONE; -bool EMSbus::tx_active_ = false; +uint8_t EMSbus::tx_state_ = Telegram::Operation::NONE; uuid::log::Logger EMSbus::logger_{F_(logger_name), uuid::log::Facility::CONSOLE}; @@ -259,19 +258,6 @@ void TxService::start() { read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER); } -// Tx loop -// here we check if the Tx is not full and report an error -void TxService::loop() { -#ifndef EMSESP_STANDALONE - if ((uuid::get_uptime() - last_tx_check_) > TX_LOOP_WAIT) { - last_tx_check_ = uuid::get_uptime(); - if (!tx_active() && (EMSbus::bus_connected())) { - LOG_ERROR(F("Tx is not active. Please check settings and the circuit connection.")); - } - } -#endif -} - // sends a 1 byte poll which is our own device ID void TxService::send_poll() { //LOG_DEBUG(F("Ack %02X"),ems_bus_id() ^ ems_mask()); @@ -381,11 +367,11 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { if (status == EMS_TX_STATUS_ERR) { LOG_ERROR(F("Failed to transmit Tx via UART.")); increment_telegram_fail_count(); // another Tx fail - tx_waiting(Telegram::Operation::NONE); // nothing send, tx not in wait state + tx_state(Telegram::Operation::NONE); // nothing send, tx not in wait state return; } - tx_waiting(telegram->operation); // tx now in a wait state + tx_state(telegram->operation); // tx now in a wait state } // send an array of bytes as a telegram @@ -399,7 +385,7 @@ void TxService::send_telegram(const uint8_t * data, const uint8_t length) { } telegram_raw[length] = calculate_crc(telegram_raw, length); // apppend CRC - tx_waiting(Telegram::Operation::NONE); // no post validation needed + tx_state(Telegram::Operation::NONE); // no post validation needed // send the telegram to the UART Tx uint16_t status = EMSuart::transmit(telegram_raw, length); diff --git a/src/telegram.h b/src/telegram.h index c7a696612..9a375d7e0 100644 --- a/src/telegram.h +++ b/src/telegram.h @@ -173,23 +173,11 @@ class EMSbus { bus_connected_ = true; } - static bool tx_active() { - return tx_active_; + static uint8_t tx_state() { + return tx_state_; } - static void tx_active(bool tx_active) { - tx_active_ = tx_active; - } - - static uint8_t tx_waiting() { - return tx_waiting_; - } - static void tx_waiting(uint8_t tx_waiting) { - tx_waiting_ = tx_waiting; - - // if NONE, then it's been reset which means we have an active Tx - if ((tx_waiting == Telegram::Operation::NONE) && !(tx_active_)) { - tx_active_ = true; - } + static void tx_state(uint8_t tx_state) { + tx_state_ = tx_state; } static uint8_t calculate_crc(const uint8_t * data, const uint8_t length); @@ -201,8 +189,7 @@ class EMSbus { static bool bus_connected_; // start assuming the bus hasn't been connected static uint8_t ems_mask_; // unset=0xFF, buderus=0x00, junkers/ht3=0x80 static uint8_t ems_bus_id_; // the bus id, which configurable and stored in settings - static uint8_t tx_waiting_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE) - static bool tx_active_; // is true is we have a working Tx connection + static uint8_t tx_state_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE) }; class RxService : public EMSbus { @@ -269,7 +256,6 @@ class TxService : public EMSbus { ~TxService() = default; void start(); - void loop(); void send(); void add(const uint8_t operation, @@ -364,7 +350,6 @@ class TxService : public EMSbus { private: uint8_t tx_telegram_id_ = 0; // queue counter - static constexpr uint32_t TX_LOOP_WAIT = 10000; // when to check if Tx is up and running (10 sec) uint32_t last_tx_check_ = 0; std::deque tx_telegrams_; From fab2fba64fd5e44d9b8ec2b7afef559e7e40ce00 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 20:29:45 +0200 Subject: [PATCH 19/66] add more mqtt logging for disonnect --- src/mqtt.cpp | 43 +++++++++++++++++++------------------------ src/mqtt.h | 1 - 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/mqtt.cpp b/src/mqtt.cpp index ce596dc66..a38bc4ec5 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -114,6 +114,7 @@ void Mqtt::loop() { } uint32_t currentMillis = uuid::get_uptime(); + // create publish messages for each of the EMS device values, adding to queue if (publish_time_ && (currentMillis - last_publish_ > publish_time_)) { last_publish_ = currentMillis; @@ -200,14 +201,6 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) { strlcpy(message, payload, len + 1); LOG_DEBUG(F("[DEBUG] Received %s => %s (length %d)"), topic, message, len); - /* - // strip out everything until the last / - char * topic_magnitude = strrchr(topic, '/'); - if (topic_magnitude != nullptr) { - topic = topic_magnitude + 1; - } - */ - // see if we have this topic in our subscription list, then call its callback handler // note: this will pick the first topic that matches, so for multiple devices of the same type it's gonna fail. Not sure if this is going to be an issue? for (const auto & mf : mqtt_subfunctions_) { @@ -264,22 +257,6 @@ void Mqtt::on_publish(uint16_t packetId) { mqtt_messages_.pop_front(); // always remove from queue, regardless if there was a successful ACK } -// builds up a topic by prefixing the hostname -// unless it's hardcoded like "homeassistant" -char * Mqtt::make_topic(char * result, const std::string & topic) { - // check for homesassistant - if (strncmp(topic.c_str(), "homeassistant/", 13) == 0) { - strlcpy(result, topic.c_str(), MQTT_TOPIC_MAX_SIZE); - return result; - } - - strlcpy(result, hostname_.c_str(), MQTT_TOPIC_MAX_SIZE); - strlcat(result, "/", MQTT_TOPIC_MAX_SIZE); - strlcat(result, topic.c_str(), MQTT_TOPIC_MAX_SIZE); - - return result; -} - void Mqtt::start() { mqttClient_ = EMSESP::esp8266React.getMqttClient(); @@ -294,6 +271,24 @@ void Mqtt::start() { mqttClient_->onConnect([this](bool sessionPresent) { on_connect(); }); + mqttClient_->onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { + LOG_INFO(F("MQTT disconnected: TCP")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { + LOG_INFO(F("MQTT disconnected: Identifier Rejected")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) { + LOG_INFO(F("MQTT disconnected: Server unavailable")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) { + LOG_INFO(F("MQTT disconnected: Malformed credentials")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { + LOG_INFO(F("MQTT disconnected: Not authorized")); + } + }); + // create will_topic with the hostname prefixed. It has to be static because asyncmqttclient destroys the reference static char will_topic[MQTT_TOPIC_MAX_SIZE]; strlcpy(will_topic, hostname_.c_str(), MQTT_TOPIC_MAX_SIZE); diff --git a/src/mqtt.h b/src/mqtt.h index c9d233c76..b0ec6b3e8 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -133,7 +133,6 @@ class Mqtt { void on_publish(uint16_t packetId); void on_message(char * topic, char * payload, size_t len); - static char * make_topic(char * result, const std::string & topic); void process_queue(); void process_all_queue(); From b18e0a1918d8607ba2717befca36da0620494922 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 20:30:03 +0200 Subject: [PATCH 20/66] force saving settings during a 'restart' --- src/system.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index 854a9edc2..05e983f8a 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -142,9 +142,9 @@ void System::mqtt_commands(const char * message) { // restart EMS-ESP void System::restart() { LOG_NOTICE("Restarting system..."); - Shell::loop_all(); - delay(1000); // wait a second + EMSESP::esp8266React.getWiFiSettingsService()->callUpdateHandlers("local"); // forces a save + delay(1000); // wait a second #if defined(ESP8266) ESP.reset(); #elif defined(ESP32) @@ -562,7 +562,7 @@ void System::console_commands(Shell & shell, unsigned int context) { wifiSettings.ssid = arguments.front().c_str(); return StateUpdateResult::CHANGED; }); - shell.println("You will need to restart to apply the new WiFi changes"); + shell.println("You will need to use the restart command to apply the new WiFi changes"); }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, From 09ab9fe6554b1942385c3adf468ce43f41427854 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 20:30:38 +0200 Subject: [PATCH 21/66] clean up web code --- src/EMSESPDevicesService.cpp | 29 +++++++++++++++++++++++------ src/EMSESPDevicesService.h | 22 ++++++++++++++++++++-- src/EMSESPSettingsService.cpp | 18 ++++++++++++++++++ src/EMSESPSettingsService.h | 20 +++++++++++++++++++- src/EMSESPStatusService.cpp | 20 +++++++++++++++++++- src/EMSESPStatusService.h | 21 ++++++++++++++++++--- 6 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/EMSESPDevicesService.cpp b/src/EMSESPDevicesService.cpp index 585b2b4c8..8b77d3b94 100644 --- a/src/EMSESPDevicesService.cpp +++ b/src/EMSESPDevicesService.cpp @@ -1,3 +1,21 @@ +/* + * EMS-ESP - https://github.com/proddy/EMS-ESP + * Copyright 2019 Paul Derbyshire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #include "EMSESPDevicesService.h" #include "emsesp.h" #include "mqtt.h" @@ -5,9 +23,8 @@ namespace emsesp { EMSESPDevicesService::EMSESPDevicesService(AsyncWebServer * server, SecurityManager * securityManager) - : _timeHandler(DEVICE_DATA_SERVICE_PATH, - securityManager->wrapCallback(std::bind(&EMSESPDevicesService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED)) { - // body + : _device_dataHandler(DEVICE_DATA_SERVICE_PATH, + securityManager->wrapCallback(std::bind(&EMSESPDevicesService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED)) { server->on(EMSESP_DEVICES_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&EMSESPDevicesService::all_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED)); @@ -16,9 +33,9 @@ EMSESPDevicesService::EMSESPDevicesService(AsyncWebServer * server, SecurityMana HTTP_GET, securityManager->wrapRequest(std::bind(&EMSESPDevicesService::scan_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED)); - _timeHandler.setMethod(HTTP_POST); - _timeHandler.setMaxContentLength(256); - server->addHandler(&_timeHandler); + _device_dataHandler.setMethod(HTTP_POST); + _device_dataHandler.setMaxContentLength(256); + server->addHandler(&_device_dataHandler); } void EMSESPDevicesService::scan_devices(AsyncWebServerRequest * request) { diff --git a/src/EMSESPDevicesService.h b/src/EMSESPDevicesService.h index db242984e..05cc58577 100644 --- a/src/EMSESPDevicesService.h +++ b/src/EMSESPDevicesService.h @@ -1,3 +1,21 @@ +/* + * EMS-ESP - https://github.com/proddy/EMS-ESP + * Copyright 2019 Paul Derbyshire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #ifndef EMSESPDevicesService_h #define EMSESPDevicesService_h @@ -23,9 +41,9 @@ class EMSESPDevicesService { private: void all_devices(AsyncWebServerRequest * request); void scan_devices(AsyncWebServerRequest * request); - - AsyncCallbackJsonWebHandler _timeHandler; void device_data(AsyncWebServerRequest * request, JsonVariant & json); + + AsyncCallbackJsonWebHandler _device_dataHandler; }; } // namespace emsesp diff --git a/src/EMSESPSettingsService.cpp b/src/EMSESPSettingsService.cpp index 0adac9027..41c5fb6a4 100644 --- a/src/EMSESPSettingsService.cpp +++ b/src/EMSESPSettingsService.cpp @@ -1,3 +1,21 @@ +/* + * EMS-ESP - https://github.com/proddy/EMS-ESP + * Copyright 2019 Paul Derbyshire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #include "EMSESPSettingsService.h" #include "emsesp.h" diff --git a/src/EMSESPSettingsService.h b/src/EMSESPSettingsService.h index 174138576..bc4f53931 100644 --- a/src/EMSESPSettingsService.h +++ b/src/EMSESPSettingsService.h @@ -1,3 +1,21 @@ +/* + * EMS-ESP - https://github.com/proddy/EMS-ESP + * Copyright 2019 Paul Derbyshire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #ifndef EMSESPSettingsConfig_h #define EMSESPSettingsConfig_h @@ -7,7 +25,7 @@ #define EMSESP_SETTINGS_FILE "/config/emsespSettings.json" #define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings" -#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0 +#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0 #define EMSESP_DEFAULT_EMS_BUS_ID 0x0B // service key #define EMSESP_DEFAULT_SYSLOG_LEVEL -1 // OFF diff --git a/src/EMSESPStatusService.cpp b/src/EMSESPStatusService.cpp index 0aaaca257..c0f6252b9 100644 --- a/src/EMSESPStatusService.cpp +++ b/src/EMSESPStatusService.cpp @@ -1,3 +1,21 @@ +/* + * EMS-ESP - https://github.com/proddy/EMS-ESP + * Copyright 2019 Paul Derbyshire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #include "EMSESPStatusService.h" #include "emsesp.h" #include "mqtt.h" @@ -12,7 +30,7 @@ EMSESPStatusService::EMSESPStatusService(AsyncWebServer * server, SecurityManage securityManager->wrapRequest(std::bind(&EMSESPStatusService::emsespStatusService, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)); -// trigger on wifi connects +// trigger on wifi connects/disconnects #ifdef ESP32 WiFi.onEvent(onStationModeDisconnected, WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); WiFi.onEvent(onStationModeGotIP, WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); diff --git a/src/EMSESPStatusService.h b/src/EMSESPStatusService.h index c2abfddb2..8ee99071a 100644 --- a/src/EMSESPStatusService.h +++ b/src/EMSESPStatusService.h @@ -1,3 +1,21 @@ +/* + * EMS-ESP - https://github.com/proddy/EMS-ESP + * Copyright 2019 Paul Derbyshire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #ifndef EMSESPStatusService_h #define EMSESPStatusService_h @@ -20,14 +38,11 @@ class EMSESPStatusService { void emsespStatusService(AsyncWebServerRequest * request); #ifdef ESP32 - static void onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info); static void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); static void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info); #elif defined(ESP8266) - WiFiEventHandler _onStationModeConnectedHandler; WiFiEventHandler _onStationModeDisconnectedHandler; WiFiEventHandler _onStationModeGotIPHandler; - static void onStationModeConnected(const WiFiEventStationModeConnected & event); static void onStationModeDisconnected(const WiFiEventStationModeDisconnected & event); static void onStationModeGotIP(const WiFiEventStationModeGotIP & event); #endif From 7109bb0d6267bfd69cc2b7a53bd4183103161335 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 21:40:56 +0200 Subject: [PATCH 22/66] merge in Michael's change from v2_uart branch --- src/telegram.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/telegram.cpp b/src/telegram.cpp index 8fa73d8e6..c9a6dc40e 100644 --- a/src/telegram.cpp +++ b/src/telegram.cpp @@ -81,9 +81,10 @@ Telegram::Telegram(const uint8_t operation, // returns telegram's message data bytes in hex std::string Telegram::to_string() const { - if (message_length == 0) { + if (this->message_length == 0) { return read_flash_string(F("")); } + uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; uint8_t length = 0; data[0] = this->src ^ RxService::ems_mask(); @@ -99,8 +100,7 @@ std::string Telegram::to_string() const { data[2] = this->type_id; length = 5; } - } - if (this->operation == Telegram::Operation::TX_WRITE) { + } else if (this->operation == Telegram::Operation::TX_WRITE) { data[1] = this->dest; if (this->type_id > 0xFF) { data[2] = 0xFF; @@ -114,7 +114,12 @@ std::string Telegram::to_string() const { for (uint8_t i = 0; i < this->message_length; i++) { data[length++] = this->message_data[i]; } + } else { + for (uint8_t i = 0; i < this->message_length; i++) { + data[length++] = this->message_data[i]; + } } + return Helpers::data_to_hex(data, length); } @@ -366,7 +371,7 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { if (status == EMS_TX_STATUS_ERR) { LOG_ERROR(F("Failed to transmit Tx via UART.")); - increment_telegram_fail_count(); // another Tx fail + increment_telegram_fail_count(); // another Tx fail tx_state(Telegram::Operation::NONE); // nothing send, tx not in wait state return; } From b7e72fd18f49b61a41e38efd9fa46ac419611ebf Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 21:41:45 +0200 Subject: [PATCH 23/66] force debug log when compiled with -DEMSESP_DEBUG --- src/console.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/console.cpp b/src/console.cpp index e5cf869de..e4e7b50ee 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -519,13 +519,15 @@ void Console::start() { // shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all #endif +#if defined(EMSESP_DEBUG) + shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all +#endif + #if defined(EMSESP_STANDALONE) // always start in su/admin mode when running tests shell->add_flags(CommandFlags::ADMIN); #endif - emsesp::EMSESP::watch(EMSESP::WATCH_OFF); // turn watch off in case it was still set in the last session - // start the telnet service // default idle is 10 minutes, default write timeout is 0 (automatic) // note, this must be started after the network/wifi for ESP32 otherwise it'll crash @@ -533,6 +535,9 @@ void Console::start() { telnet_.start(); telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second #endif + + // turn watch off in case it was still set in the last session + emsesp::EMSESP::watch(EMSESP::WATCH_OFF); } // handles telnet sync and logging to console From 5c354af1ba32d2578d51f187e42034f16352d0b9 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 22:35:33 +0200 Subject: [PATCH 24/66] added system command `wifi reconnect` --- README.md | 1 + src/system.cpp | 71 ++++++++++++++++++++++++++++++-------------------- src/system.h | 1 + 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index adf4c9f28..c7d82605a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ system set wifi hostname set wifi password set wifi ssid + wifi reconnect boiler comfort diff --git a/src/system.cpp b/src/system.cpp index 05e983f8a..bff937bb3 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -24,6 +24,7 @@ MAKE_PSTR_WORD(passwd) MAKE_PSTR_WORD(hostname) MAKE_PSTR_WORD(wifi) +MAKE_PSTR_WORD(reconnect) MAKE_PSTR_WORD(ssid) MAKE_PSTR_WORD(heartbeat) MAKE_PSTR_WORD(users) @@ -143,8 +144,7 @@ void System::mqtt_commands(const char * message) { void System::restart() { LOG_NOTICE("Restarting system..."); Shell::loop_all(); - EMSESP::esp8266React.getWiFiSettingsService()->callUpdateHandlers("local"); // forces a save - delay(1000); // wait a second + delay(1000); // wait a second #if defined(ESP8266) ESP.reset(); #elif defined(ESP32) @@ -152,10 +152,19 @@ void System::restart() { #endif } +// saves all settings +void System::wifi_reconnect() { + LOG_NOTICE("The wifi will reconnect..."); + Shell::loop_all(); + delay(1000); // wait a second + EMSESP::emsespSettingsService.save(); // local settings + EMSESP::esp8266React.getWiFiSettingsService()->callUpdateHandlers("local"); // in case we've changed ssid or password +} + // format fs // format the FS. Wipes everything. void System::format(uuid::console::Shell & shell) { - auto msg = F("Formatting file system. This will also reset all settings to their defaults"); + auto msg = F("Formatting file system. This will reset all settings to their defaults"); shell.logger().warning(msg); shell.flush(); @@ -486,6 +495,13 @@ void System::console_commands(Shell & shell, unsigned int context) { restart(); }); + EMSESPShell::commands->add_command(ShellContext::SYSTEM, + CommandFlags::ADMIN, + flash_string_vector{F_(wifi), F_(reconnect)}, + [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) { + wifi_reconnect(); + }); + EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::ADMIN, flash_string_vector{F_(format)}, @@ -562,33 +578,32 @@ void System::console_commands(Shell & shell, unsigned int context) { wifiSettings.ssid = arguments.front().c_str(); return StateUpdateResult::CHANGED; }); - shell.println("You will need to use the restart command to apply the new WiFi changes"); + shell.println("Use `wifi reconnect` to apply the new settings"); }); - EMSESPShell::commands->add_command(ShellContext::SYSTEM, - CommandFlags::ADMIN, - flash_string_vector{F_(set), F_(wifi), F_(password)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { - if (completed) { - shell.enter_password(F_(new_password_prompt2), - [password1](Shell & shell, bool completed, const std::string & password2) { - if (completed) { - if (password1 == password2) { - EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation( - [&](WiFiSettings & wifiSettings) { - wifiSettings.password = password2.c_str(); - return StateUpdateResult::CHANGED; - }); - shell.println("You will need to restart to apply the new WiFi changes"); - } else { - shell.println(F("Passwords do not match")); - } - } - }); - } - }); - }); + EMSESPShell::commands + ->add_command(ShellContext::SYSTEM, + CommandFlags::ADMIN, + flash_string_vector{F_(set), F_(wifi), F_(password)}, + [](Shell & shell, const std::vector & arguments __attribute__((unused))) { + shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { + if (completed) { + shell.enter_password(F_(new_password_prompt2), [password1](Shell & shell, bool completed, const std::string & password2) { + if (completed) { + if (password1 == password2) { + EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation([&](WiFiSettings & wifiSettings) { + wifiSettings.password = password2.c_str(); + return StateUpdateResult::CHANGED; + }); + shell.println("Use `wifi reconnect` to apply the new settings"); + } else { + shell.println(F("Passwords do not match")); + } + } + }); + } + }); + }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::USER, diff --git a/src/system.h b/src/system.h index 3aa20de2e..e46965017 100644 --- a/src/system.h +++ b/src/system.h @@ -98,6 +98,7 @@ class System { static void show_system(uuid::console::Shell & shell); static void show_users(uuid::console::Shell & shell); + static void wifi_reconnect(); static int8_t wifi_quality(); bool system_healthy_ = false; From 2ec35d1fbc622177a5a0d6bb7d802796b36c7c7c Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 22:35:55 +0200 Subject: [PATCH 25/66] added save function to write settings to fS --- src/EMSESPSettingsService.cpp | 4 ++++ src/EMSESPSettingsService.h | 1 + 2 files changed, 5 insertions(+) diff --git a/src/EMSESPSettingsService.cpp b/src/EMSESPSettingsService.cpp index 41c5fb6a4..1f08c4760 100644 --- a/src/EMSESPSettingsService.cpp +++ b/src/EMSESPSettingsService.cpp @@ -82,4 +82,8 @@ void EMSESPSettingsService::begin() { _fsPersistence.readFromFS(); } +void EMSESPSettingsService::save() { + _fsPersistence.writeToFS(); +} + } // namespace emsesp \ No newline at end of file diff --git a/src/EMSESPSettingsService.h b/src/EMSESPSettingsService.h index bc4f53931..8e9ff8e4e 100644 --- a/src/EMSESPSettingsService.h +++ b/src/EMSESPSettingsService.h @@ -62,6 +62,7 @@ class EMSESPSettingsService : public StatefulService { EMSESPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager); void begin(); + void save(); private: HttpEndpoint _httpEndpoint; From b59d711ed41ddbde71568fd7d403a64cca0804fe Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 26 Jul 2020 22:36:18 +0200 Subject: [PATCH 26/66] merged Michael's latest UART code for ESP8266 --- src/uart/emsuart_esp8266.cpp | 22 ++++++++++++++++------ src/uart/emsuart_esp8266.h | 8 +++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/uart/emsuart_esp8266.cpp b/src/uart/emsuart_esp8266.cpp index dfc0f758f..f168d3f13 100644 --- a/src/uart/emsuart_esp8266.cpp +++ b/src/uart/emsuart_esp8266.cpp @@ -114,7 +114,11 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_flush_fifos() { * init UART0 driver */ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { - emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode + 10); // bittimes wait between bytes + if (tx_mode_ > 100) { + emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode - 90); + } else { + emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode + 10); // bittimes wait to next bytes + } if (tx_mode_ != 0xFF) { // it's a restart no need to configure uart tx_mode_ = tx_mode; restart(); @@ -175,12 +179,12 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { system_uart_swap(); ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr); - // ETS_UART_INTR_ENABLE(); drop_next_rx = true; // for sending with large delay in EMS+ mode we use a timer interrupt timer1_attachInterrupt(emsuart_tx_timer_intr_handler); // Add ISR Function timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); // 5 MHz timer + ETS_UART_INTR_ENABLE(); USIE(EMSUART_UART) = (1 << UIBD); } @@ -190,7 +194,9 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { */ void ICACHE_FLASH_ATTR EMSuart::stop() { USIE(EMSUART_UART) = 0; - // timer1_disable(); + USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear BRK bit + timer1_disable(); + sending_ = false; } /* @@ -203,7 +209,7 @@ void ICACHE_FLASH_ATTR EMSuart::restart() { } emsTxBufIdx = 0; emsTxBufLen = 0; - // timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); USIE(EMSUART_UART) = (1 << UIBD); } @@ -282,7 +288,11 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { USF(EMSUART_UART) = buf[0]; // send first byte emsTxBufIdx = 0; emsTxBufLen = len; - timer1_write(emsTxWait); + if (tx_mode_ > 100) { + timer1_write(EMSUART_TX_WAIT_REPLY); + } else { + timer1_write(emsTxWait); + } return EMS_TX_STATUS_OK; } @@ -378,4 +388,4 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { } // namespace emsesp -#endif +#endif \ No newline at end of file diff --git a/src/uart/emsuart_esp8266.h b/src/uart/emsuart_esp8266.h index 29f0c0eb1..01407e54e 100644 --- a/src/uart/emsuart_esp8266.h +++ b/src/uart/emsuart_esp8266.h @@ -41,12 +41,14 @@ // LEGACY #define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud -#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 11) // 1144 +#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 10) +#define EMSUART_TX_WAIT_REPLY 500000 // delay 100ms after first byte // EMS 1.0 #define EMSUART_TX_BUSY_WAIT (EMSUART_TX_BIT_TIME / 8) // 13 // #define EMSUART_TX_TIMEOUT (22 * EMSUART_TX_BIT_TIME / EMSUART_TX_BUSY_WAIT) // 176 -#define EMSUART_TX_TIMEOUT (32 * 8) // 256 for tx_mode 1 - see https://github.com/proddy/EMS-ESP/issues/398#issuecomment-645886277 +// #define EMSUART_TX_TIMEOUT (32 * 8) // 256 for tx_mode 1 - see https://github.com/proddy/EMS-ESP/issues/398#issuecomment-645886277 +#define EMSUART_TX_TIMEOUT (220 * 8) // 1760 as in v1.9 (180 ms) // HT3/Junkers - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) plus 7 bit delay. The -8 is for lag compensation. // since we use a faster processor the lag is negligible @@ -91,4 +93,4 @@ class EMSuart { } // namespace emsesp #endif -#endif +#endif \ No newline at end of file From fe13b7559d11d746f572e40257ed6815dc4529ce Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:54:24 +0200 Subject: [PATCH 27/66] renamed telegram MaintenanceSettings to MaintenanceData --- src/devices/boiler.cpp | 7 +++++-- src/devices/boiler.h | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 637c60bba..acac09422 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -59,7 +59,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const register_telegram_type(0x33, F("UBAParameterWW"), true, std::bind(&Boiler::process_UBAParameterWW, this, _1)); register_telegram_type(0x14, F("UBATotalUptime"), false, std::bind(&Boiler::process_UBATotalUptime, this, _1)); register_telegram_type(0x35, F("UBAFlags"), false, std::bind(&Boiler::process_UBAFlags, this, _1)); - register_telegram_type(0x15, F("UBAMaintenanceSettings"), false, std::bind(&Boiler::process_UBAMaintenanceSettings, this, _1)); + register_telegram_type(0x15, F("UBAMaintenanceData"), false, std::bind(&Boiler::process_UBAMaintenanceData, this, _1)); register_telegram_type(0x16, F("UBAParameters"), true, std::bind(&Boiler::process_UBAParameters, this, _1)); register_telegram_type(0x1A, F("UBASetPoints"), false, std::bind(&Boiler::process_UBASetPoints, this, _1)); register_telegram_type(0xD1, F("UBAOutdoorTemp"), false, std::bind(&Boiler::process_UBAOutdoorTemp, this, _1)); @@ -688,11 +688,14 @@ void Boiler::process_UBAFlags(std::shared_ptr telegram) { // 0x1C // not yet implemented void Boiler::process_UBAMaintenanceStatus(std::shared_ptr telegram) { + // first byte: Maintenance due (0 = no, 3 = yes, due to operating hours, 8 = yes, due to date) } // 0x15 // not yet implemented -void Boiler::process_UBAMaintenanceSettings(std::shared_ptr telegram) { +void Boiler::process_UBAMaintenanceData(std::shared_ptr telegram) { + // first byte: Maintenance messages (0 = none, 1 = by operating hours, 2 = by date) + // I see a value of 3 when the boiler is booted, so probably a flag } // 0x10, 0x11, 0x12 diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 60e4170e1..ead6431c7 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -141,7 +141,7 @@ class Boiler : public EMSdevice { void process_UBAFlags(std::shared_ptr telegram); void process_MC10Status(std::shared_ptr telegram); void process_UBAMaintenanceStatus(std::shared_ptr telegram); - void process_UBAMaintenanceSettings(std::shared_ptr telegram); + void process_UBAMaintenanceData(std::shared_ptr telegram); void process_UBAErrorMessage(std::shared_ptr telegram); void process_UBADHWStatus(std::shared_ptr telegram); From 63737833ae16745b80efbdc919678e769ea936ac Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:54:53 +0200 Subject: [PATCH 28/66] renamed command refresh to fetch --- src/console.cpp | 5 ++--- src/console.h | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/console.cpp b/src/console.cpp index e4e7b50ee..830af193d 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -107,7 +107,7 @@ void EMSESPShell::add_console_commands() { commands->add_command(ShellContext::MAIN, CommandFlags::USER, - flash_string_vector{F_(refresh)}, + flash_string_vector{F_(fetch)}, [&](Shell & shell, const std::vector & arguments __attribute__((unused))) { shell.printfln(F("Requesting data from EMS devices")); console_commands_loaded_ = false; @@ -177,11 +177,10 @@ void EMSESPShell::add_console_commands() { flash_string_vector{F_(n_mandatory)}, [](Shell & shell, const std::vector & arguments) { uint8_t tx_mode = std::strtol(arguments[0].c_str(), nullptr, 10); - + // save the tx_mode EMSESP::emsespSettingsService.update( [&](EMSESPSettings & settings) { settings.tx_mode = tx_mode; - shell.printfln(F_(tx_mode_fmt), tx_mode); return StateUpdateResult::CHANGED; }, "local"); diff --git a/src/console.h b/src/console.h index 5b335503c..e7458abf0 100644 --- a/src/console.h +++ b/src/console.h @@ -73,7 +73,7 @@ MAKE_PSTR_WORD(read) MAKE_PSTR_WORD(version) MAKE_PSTR_WORD(values) MAKE_PSTR_WORD(system) -MAKE_PSTR_WORD(refresh) +MAKE_PSTR_WORD(fetch) MAKE_PSTR_WORD(restart) MAKE_PSTR_WORD(format) MAKE_PSTR_WORD(raw) From bb3e8b6dffed1a36055ed6db727e2a4027889f33 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:55:03 +0200 Subject: [PATCH 29/66] added fetch command --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7d82605a..4e2f0e16f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ ## **Breaking changes** -- MQTT base has been removed. The hostname is only used. +- MQTT base has been removed. The hostname is only used +- refresh command renamed to fetch +- have to 'wifi reconnect' after changing wifi in console ## **New Features in v2** @@ -65,7 +67,7 @@ common commands available in all contexts: (from the root) set - refresh + fetch system (enters a context) boiler (enters a context) thermostat (enters a context) From 8caa452c25e38ed10d4a6c31fbff139936566da4 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:55:30 +0200 Subject: [PATCH 30/66] move mem calc before creating json obj --- lib/framework/SystemStatus.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/framework/SystemStatus.cpp b/lib/framework/SystemStatus.cpp index 1a5c1c6df..017e5c723 100644 --- a/lib/framework/SystemStatus.cpp +++ b/lib/framework/SystemStatus.cpp @@ -7,6 +7,8 @@ SystemStatus::SystemStatus(AsyncWebServer * server, SecurityManager * securityMa } void SystemStatus::systemStatus(AsyncWebServerRequest * request) { + uint8_t free_mem_percent = emsesp::System::free_mem(); // added by proddy + AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_ESP_STATUS_SIZE); JsonObject root = response->getRoot(); #ifdef ESP32 @@ -39,7 +41,7 @@ void SystemStatus::systemStatus(AsyncWebServerRequest * request) { #endif root["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); // proddy added - root["free_mem"] = emsesp::System::free_mem(); // proddy added + root["free_mem"] = free_mem_percent; // proddy added response->setLength(); request->send(response); From b062f30bddb77524b29a4dfcfe566eb4e0c5475d Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:56:24 +0200 Subject: [PATCH 31/66] tx_mode optimizations --- src/system.cpp | 51 ++++++++++++++++++++++++------------------------ src/telegram.cpp | 25 ++++++++++++++++++++---- src/telegram.h | 42 +++++++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index bff937bb3..45dd90758 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -245,9 +245,7 @@ void System::start() { } #ifndef EMSESP_FORCE_SERIAL - if (tx_mode_) { - EMSuart::start(tx_mode_); // start UART, if tx_mode is not 0 - } + EMSuart::start(tx_mode_); // start UART #endif } @@ -581,29 +579,30 @@ void System::console_commands(Shell & shell, unsigned int context) { shell.println("Use `wifi reconnect` to apply the new settings"); }); - EMSESPShell::commands - ->add_command(ShellContext::SYSTEM, - CommandFlags::ADMIN, - flash_string_vector{F_(set), F_(wifi), F_(password)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { - if (completed) { - shell.enter_password(F_(new_password_prompt2), [password1](Shell & shell, bool completed, const std::string & password2) { - if (completed) { - if (password1 == password2) { - EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation([&](WiFiSettings & wifiSettings) { - wifiSettings.password = password2.c_str(); - return StateUpdateResult::CHANGED; - }); - shell.println("Use `wifi reconnect` to apply the new settings"); - } else { - shell.println(F("Passwords do not match")); - } - } - }); - } - }); - }); + EMSESPShell::commands->add_command(ShellContext::SYSTEM, + CommandFlags::ADMIN, + flash_string_vector{F_(set), F_(wifi), F_(password)}, + [](Shell & shell, const std::vector & arguments __attribute__((unused))) { + shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { + if (completed) { + shell.enter_password(F_(new_password_prompt2), + [password1](Shell & shell, bool completed, const std::string & password2) { + if (completed) { + if (password1 == password2) { + EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation( + [&](WiFiSettings & wifiSettings) { + wifiSettings.password = password2.c_str(); + return StateUpdateResult::CHANGED; + }); + shell.println("Use `wifi reconnect` to apply the new settings"); + } else { + shell.println(F("Passwords do not match")); + } + } + }); + } + }); + }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::USER, diff --git a/src/telegram.cpp b/src/telegram.cpp index c9a6dc40e..002374d75 100644 --- a/src/telegram.cpp +++ b/src/telegram.cpp @@ -42,6 +42,7 @@ uint32_t EMSbus::last_bus_activity_ = 0; // timestamp of last time bool EMSbus::bus_connected_ = false; // start assuming the bus hasn't been connected uint8_t EMSbus::ems_mask_ = EMS_MASK_UNSET; // unset so its triggered when booting, the its 0x00=buderus, 0x80=junker/ht3 uint8_t EMSbus::ems_bus_id_ = EMSESP_DEFAULT_EMS_BUS_ID; +uint8_t EMSbus::tx_mode_ = EMSESP_DEFAULT_TX_MODE; uint8_t EMSbus::tx_state_ = Telegram::Operation::NONE; uuid::log::Logger EMSbus::logger_{F_(logger_name), uuid::log::Facility::CONSOLE}; @@ -255,8 +256,16 @@ void TxService::flush_tx_queue() { // start and initialize Tx void TxService::start() { - // grab the bus ID - EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { ems_bus_id(settings.ems_bus_id); }); + // grab the bus ID and tx_mode + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { + ems_bus_id(settings.ems_bus_id); + tx_mode(settings.tx_mode); + }); + + // reset counters + telegram_read_count(0); + telegram_write_count(0); + telegram_fail_count(0); // send first Tx request to bus master (boiler) for its registered devices // this will be added to the queue and sent during the first tx loop() @@ -266,24 +275,32 @@ void TxService::start() { // sends a 1 byte poll which is our own device ID void TxService::send_poll() { //LOG_DEBUG(F("Ack %02X"),ems_bus_id() ^ ems_mask()); - EMSuart::send_poll(ems_bus_id() ^ ems_mask()); + if (tx_mode()) { + EMSuart::send_poll(ems_bus_id() ^ ems_mask()); + } } // Process the next telegram on the Tx queue // This is sent when we receieve a poll request void TxService::send() { // don't process if we don't have a connection to the EMS bus - // or we're in read-only mode if (!bus_connected()) { return; } // if there's nothing in the queue to transmit, send back a poll and quit + // unless tx_mode is 0 if (tx_telegrams_.empty()) { send_poll(); return; } + // if we're in read-only mode (tx_mode 0) forget the Tx call + if (tx_mode() == 0) { + tx_telegrams_.pop_front(); + return; + } + // send next telegram in the queue (which is actually a list!) send_telegram(tx_telegrams_.front()); diff --git a/src/telegram.h b/src/telegram.h index 9a375d7e0..fa19c2c5f 100644 --- a/src/telegram.h +++ b/src/telegram.h @@ -126,22 +126,10 @@ class EMSbus { public: static uuid::log::Logger logger_; - static constexpr uint8_t EMS_MASK_UNSET = 0xFF; // EMS bus type (budrus/junkers) hasn't been detected yet - static constexpr uint8_t EMS_MASK_HT3 = 0x80; // EMS bus type Junkers/HT3 - static constexpr uint8_t EMS_MASK_BUDERUS = 0xFF; // EMS bus type Buderus - - static constexpr uint8_t EMS_TX_ERROR_LIMIT = 10; // % limit of failed Tx read/write attempts before showing a warning - - static bool bus_connected() { -#ifndef EMSESP_STANDALONE - if ((uuid::get_uptime() - last_bus_activity_) > EMS_BUS_TIMEOUT) { - bus_connected_ = false; - } - return bus_connected_; -#else - return true; -#endif - } + static constexpr uint8_t EMS_MASK_UNSET = 0xFF; // EMS bus type (budrus/junkers) hasn't been detected yet + static constexpr uint8_t EMS_MASK_HT3 = 0x80; // EMS bus type Junkers/HT3 + static constexpr uint8_t EMS_MASK_BUDERUS = 0xFF; // EMS bus type Buderus + static constexpr uint8_t EMS_TX_ERROR_LIMIT = 10; // % limit of failed Tx read/write attempts before showing a warning static bool is_ht3() { return (ems_mask_ == EMS_MASK_HT3); @@ -159,6 +147,14 @@ class EMSbus { ems_mask_ = ems_mask & 0x80; // only keep the MSB (8th bit) } + static uint8_t tx_mode() { + return tx_mode_; + } + + static void tx_mode(uint8_t tx_mode) { + tx_mode_ = tx_mode; + } + static uint8_t ems_bus_id() { return ems_bus_id_; } @@ -167,6 +163,17 @@ class EMSbus { ems_bus_id_ = ems_bus_id; } + static bool bus_connected() { +#ifndef EMSESP_STANDALONE + if ((uuid::get_uptime() - last_bus_activity_) > EMS_BUS_TIMEOUT) { + bus_connected_ = false; + } + return bus_connected_; +#else + return true; +#endif + } + // sets the flag for EMS bus connected static void last_bus_activity(uint32_t timestamp) { last_bus_activity_ = timestamp; @@ -189,6 +196,7 @@ class EMSbus { static bool bus_connected_; // start assuming the bus hasn't been connected static uint8_t ems_mask_; // unset=0xFF, buderus=0x00, junkers/ht3=0x80 static uint8_t ems_bus_id_; // the bus id, which configurable and stored in settings + static uint8_t tx_mode_; // local copy of the tx mode static uint8_t tx_state_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE) }; @@ -350,7 +358,7 @@ class TxService : public EMSbus { private: uint8_t tx_telegram_id_ = 0; // queue counter - uint32_t last_tx_check_ = 0; + uint32_t last_tx_check_ = 0; std::deque tx_telegrams_; From ced2d2df4749104ec6b86c94cc1a1b0fd084bc7f Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:56:37 +0200 Subject: [PATCH 32/66] tx_mode optimizations --- src/emsesp.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 5cc61a255..a0f55df89 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -111,12 +111,13 @@ void EMSESP::watch_id(uint16_t watch_id) { // change the tx_mode // resets all counters and bumps the UART void EMSESP::reset_tx(uint8_t const tx_mode) { - txservice_.telegram_read_count(0); - txservice_.telegram_write_count(0); - txservice_.telegram_fail_count(0); - if (tx_mode) { - EMSuart::stop(); - EMSuart::start(tx_mode); + EMSuart::stop(); + + txservice_.start(); + + EMSuart::start(tx_mode); + + if (tx_mode != 0) { EMSESP::fetch_device_values(); } } @@ -773,6 +774,7 @@ void EMSESP::loop() { // if we're doing an OTA upload, skip MQTT and EMS if (system_.upload_status()) { + delay(1); // slow down OTA update to avoid getting killed by task watchdog (task_wdt) return; } From f5b214a1b29a3bb6dcf8ad985dfd70b0dc6238b7 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:56:52 +0200 Subject: [PATCH 33/66] added data to web ui --- src/devices/mixing.cpp | 15 +++++++++++++++ src/devices/solar.cpp | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/devices/mixing.cpp b/src/devices/mixing.cpp index c4ebe6ced..7fc8e4424 100644 --- a/src/devices/mixing.cpp +++ b/src/devices/mixing.cpp @@ -57,7 +57,22 @@ Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const void Mixing::add_context_menu() { } +// output json to web UI void Mixing::device_info(JsonArray & root) { + if (type_ == Type::NONE) { + return; // don't have any values yet + } + + if (type_ == Type::WWC) { + render_value_json(root, "", F("Warm Water Circuit"), hc_, nullptr); + } else { + render_value_json(root, "", F("Heating Circuit"), hc_, nullptr); + } + render_value_json(root, "", F("Current flow temperature"), flowTemp_, F_(degrees), 10); + render_value_json(root, "", F("Setpoint flow temperature"), flowSetTemp_, F_(degrees)); + render_value_json(root, "", F("Current pump modulation"), pumpMod_, F_(percent)); + render_value_json(root, "", F("Current valve status"), status_, nullptr); + } // check to see if values have been updated diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index d3e9e0c09..2d5c55681 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -58,7 +58,30 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s void Solar::add_context_menu() { } +// print to web void Solar::device_info(JsonArray & root) { + render_value_json(root, "", F("Collector temperature (TS1)"), collectorTemp_, F_(degrees), 10); + render_value_json(root, "", F("Bottom temperature (TS2)"), bottomTemp_, F_(degrees), 10); + render_value_json(root, "", F("Bottom temperature (TS5)"), bottomTemp2_, F_(degrees), 10); + render_value_json(root, "", F("Pump modulation"), pumpModulation_, F_(percent)); + render_value_json(root, "", F("Valve (VS2) status"), valveStatus_, nullptr, EMS_VALUE_BOOL); + render_value_json(root, "", F("Pump (PS1) active"), pump_, nullptr, EMS_VALUE_BOOL); + + if (Helpers::hasValue(pumpWorkMin_)) { + JsonObject dataElement; + dataElement = root.createNestedObject(); + dataElement["name"] = F("Pump working time"); + std::string time_str(60, '\0'); + snprintf_P(&time_str[0], time_str.capacity() + 1, PSTR("%d days %d hours %d minutes"), pumpWorkMin_ / 1440, (pumpWorkMin_ % 1440) / 60, pumpWorkMin_ % 60); + dataElement["value"] = time_str; + } + + render_value_json(root, "", F("Tank Heated"), tankHeated_, nullptr, EMS_VALUE_BOOL); + render_value_json(root, "", F("Collector"), collectorOnOff_, nullptr, EMS_VALUE_BOOL); + + render_value_json(root, "", F("Energy last hour"), energyLastHour_, F_(wh), 10); + render_value_json(root, "", F("Energy today"), energyToday_, F_(wh)); + render_value_json(root, "", F("Energy total"), energyTotal_, F_(kwh), 10); } // display all values into the shell console From 9b7d6aa6edd067ce2201a45fc145d1f6b5bb1328 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:57:05 +0200 Subject: [PATCH 34/66] minor change --- src/devices/thermostat.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index 2978bab35..85916655f 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -171,8 +171,6 @@ void Thermostat::init_mqtt() { // prepare data for Web UI void Thermostat::device_info(JsonArray & root) { - JsonObject dataElement; - uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, strip the option bits for (const auto & hc : heating_circuits_) { @@ -196,7 +194,7 @@ void Thermostat::device_info(JsonArray & root) { format_curr = 10; // *10 break; } - + // create prefix with heating circuit number std::string hc_str(5, '\0'); snprintf_P(&hc_str[0], hc_str.capacity() + 1, PSTR("hc%d: "), hc->hc_num()); @@ -204,6 +202,7 @@ void Thermostat::device_info(JsonArray & root) { render_value_json(root, hc_str, F("Current room temperature"), hc->curr_roomTemp, F_(degrees), format_curr); render_value_json(root, hc_str, F("Setpoint room temperature"), hc->setpoint_roomTemp, F_(degrees), format_setpoint); if (Helpers::hasValue(hc->mode)) { + JsonObject dataElement; dataElement = root.createNestedObject(); std::string mode_str(15, '\0'); snprintf_P(&mode_str[0], mode_str.capacity() + 1, PSTR("%sMode"), hc_str.c_str()); From b196fbd0fd6c21b4dd3012dbfc9077e1587d740f Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 27 Jul 2020 18:57:26 +0200 Subject: [PATCH 35/66] merged from v2_uart --- src/uart/emsuart_esp32.cpp | 28 ++++++++++++++++++---------- src/uart/emsuart_esp32.h | 5 +++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/uart/emsuart_esp32.cpp b/src/uart/emsuart_esp32.cpp index b827d2328..e699bb59b 100644 --- a/src/uart/emsuart_esp32.cpp +++ b/src/uart/emsuart_esp32.cpp @@ -88,11 +88,11 @@ void IRAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() { portENTER_CRITICAL(&mux); if (emsTxBufIdx < emsTxBufLen) { EMS_UART.fifo.rw_byte = emsTxBuf[emsTxBufIdx]; + timerAlarmWrite(timer, emsTxWait, true); } else if (emsTxBufIdx == emsTxBufLen) { EMS_UART.conf0.txd_inv = 1; timerAlarmWrite(timer, EMSUART_TX_WAIT_BRK, true); } else if (emsTxBufIdx == emsTxBufLen + 1) { - // delayMicroseconds(EMSUART_TX_WAIT_BRK); EMS_UART.conf0.txd_inv = 0; timerAlarmDisable(timer); } @@ -122,28 +122,28 @@ void EMSuart::start(const uint8_t tx_mode) { uart_set_pin(EMSUART_UART, EMSUART_TXPIN, EMSUART_RXPIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); EMS_UART.int_ena.val = 0; // disable all intr. EMS_UART.int_clr.val = 0xFFFFFFFF; // clear all intr. flags - EMS_UART.idle_conf.tx_brk_num = 11; // breaklength 11 bit + EMS_UART.idle_conf.tx_brk_num = 10; // breaklength 10 bit EMS_UART.idle_conf.rx_idle_thrhd = 256; drop_next_rx = true; buf_handle = xRingbufferCreate(128, RINGBUF_TYPE_NOSPLIT); uart_isr_register(EMSUART_UART, emsuart_rx_intr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL); xTaskCreate(emsuart_recvTask, "emsuart_recvTask", 2048, NULL, configMAX_PRIORITIES - 1, NULL); - timer = timerBegin(1, 80, true); // timer prescale to 1 µs, countup - timerAttachInterrupt(timer, &emsuart_tx_timer_intr_handler, true); // Timer with edge interrupt + timer = timerBegin(0, 80, true); // timer prescale to 1 us, countup + timerAttachInterrupt(timer, &emsuart_tx_timer_intr_handler, false); // Timer with level interrupt restart(); } /* - * Stop, disables interrupt + * Stop, disable interrupt */ void EMSuart::stop() { - EMS_UART.int_ena.val = 0; // disable all intr. - // timerAlarmDisable(timer); + EMS_UART.int_ena.val = 0; // disable all intr. + EMS_UART.conf0.txd_inv = 0; // stop break }; /* - * Restart Interrupt + * Restart uart and make mode dependent configs. */ void EMSuart::restart() { if (EMS_UART.int_raw.brk_det) { // we received a break in the meantime @@ -153,7 +153,11 @@ void EMSuart::restart() { EMS_UART.int_ena.brk_det = 1; // activate only break emsTxBufIdx = 0; emsTxBufLen = 0; - emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ + 10); + if (tx_mode_ > 100) { + emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ - 90); + } else { + emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ + 10); + } if(tx_mode_ == EMS_TXMODE_NEW) { EMS_UART.conf0.txd_brk = 1; } else { @@ -217,7 +221,11 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { } emsTxBufIdx = 0; emsTxBufLen = len; - timerAlarmWrite(timer, emsTxWait, true); // start with autoreload + if (tx_mode_ > 100) { + timerAlarmWrite(timer, EMSUART_TX_WAIT_REPLY, true); + } else { + timerAlarmWrite(timer, emsTxWait, true); // start with autoreload + } timerAlarmEnable(timer); return EMS_TX_STATUS_OK; } diff --git a/src/uart/emsuart_esp32.h b/src/uart/emsuart_esp32.h index 043a0ae4c..ab2d1fd45 100644 --- a/src/uart/emsuart_esp32.h +++ b/src/uart/emsuart_esp32.h @@ -47,7 +47,8 @@ // LEGACY #define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud -#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 11) // 1144 +#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 10) // 10 bt +#define EMSUART_TX_WAIT_REPLY 100000 // delay 100ms after first byte // EMS 1.0 #define EMSUART_TX_BUSY_WAIT (EMSUART_TX_BIT_TIME / 8) // 13 @@ -96,4 +97,4 @@ class EMSuart { } // namespace emsesp #endif -#endif +#endif \ No newline at end of file From 53aa698bbbb858cf2f9b69f186617b21aedbc711 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 11:50:31 +0200 Subject: [PATCH 36/66] test code only for standalone mode --- src/test/test.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/test.h b/src/test/test.h index 300ec8796..c45fe42d4 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +#if defined(EMSESP_STANDALONE) + #ifndef EMSESP_TEST_H #define EMSESP_TEST_H @@ -51,3 +53,5 @@ class Test { } // namespace emsesp #endif + +#endif From 3ab7134ca0eb19b3d376105b9c302aedda4aacf8 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 11:52:23 +0200 Subject: [PATCH 37/66] reset_tx() takes tx_mode from settings file --- src/console.cpp | 5 +---- src/emsesp.cpp | 11 +++++++---- src/emsesp.h | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/console.cpp b/src/console.cpp index 830af193d..a4ede0465 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -110,8 +110,6 @@ void EMSESPShell::add_console_commands() { flash_string_vector{F_(fetch)}, [&](Shell & shell, const std::vector & arguments __attribute__((unused))) { shell.printfln(F("Requesting data from EMS devices")); - console_commands_loaded_ = false; - add_console_commands(); EMSESP::fetch_device_values(); }); @@ -184,7 +182,7 @@ void EMSESPShell::add_console_commands() { return StateUpdateResult::CHANGED; }, "local"); - EMSESP::reset_tx(tx_mode); // reset counters and set tx_mode + EMSESP::reset_tx(); // reset counters and set tx_mode }); commands->add_command(ShellContext::MAIN, @@ -515,7 +513,6 @@ void Console::start() { shell = std::make_shared(serial_console_, true); shell->maximum_log_messages(100); // default is 50 shell->start(); - // shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all #endif #if defined(EMSESP_DEBUG) diff --git a/src/emsesp.cpp b/src/emsesp.cpp index a0f55df89..e779cbcf4 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -110,13 +110,16 @@ void EMSESP::watch_id(uint16_t watch_id) { // change the tx_mode // resets all counters and bumps the UART -void EMSESP::reset_tx(uint8_t const tx_mode) { - EMSuart::stop(); +void EMSESP::reset_tx() { + // get the tx_mode + uint8_t tx_mode; + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode = settings.tx_mode; }); + EMSuart::stop(); + EMSuart::start(tx_mode); txservice_.start(); - EMSuart::start(tx_mode); - + // force a fetch for all new values, unless Tx is set to off if (tx_mode != 0) { EMSESP::fetch_device_values(); } diff --git a/src/emsesp.h b/src/emsesp.h index 28d62c860..e22633f88 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -96,7 +96,7 @@ class EMSESP { static void add_context_menus(); - static void reset_tx(uint8_t const tx_mode); + static void reset_tx(); static void incoming_telegram(uint8_t * data, const uint8_t length); From 242770838dfca2f20ade52ba7782d8bd9186f289 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 11:52:56 +0200 Subject: [PATCH 38/66] call services after settings have been persisted --- src/EMSESPSettingsService.cpp | 62 ++++++++++++----------------------- src/EMSESPSettingsService.h | 2 ++ 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/src/EMSESPSettingsService.cpp b/src/EMSESPSettingsService.cpp index 1f08c4760..9cf40cf9e 100644 --- a/src/EMSESPSettingsService.cpp +++ b/src/EMSESPSettingsService.cpp @@ -24,58 +24,38 @@ namespace emsesp { EMSESPSettingsService::EMSESPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager) : _httpEndpoint(EMSESPSettings::read, EMSESPSettings::update, this, server, EMSESP_SETTINGS_SERVICE_PATH, securityManager) , _fsPersistence(EMSESPSettings::read, EMSESPSettings::update, this, fs, EMSESP_SETTINGS_FILE) { + addUpdateHandler([&](const String & originId) { onUpdate(); }, false); } void EMSESPSettings::read(EMSESPSettings & settings, JsonObject & root) { - root["tx_mode"] = settings.tx_mode; - root["ems_bus_id"] = settings.ems_bus_id; - + root["tx_mode"] = settings.tx_mode; + root["ems_bus_id"] = settings.ems_bus_id; root["syslog_level"] = settings.syslog_level; root["syslog_mark_interval"] = settings.syslog_mark_interval; root["syslog_host"] = settings.syslog_host; - - root["master_thermostat"] = settings.master_thermostat; - root["shower_timer"] = settings.shower_timer; - root["shower_alert"] = settings.shower_alert; + root["master_thermostat"] = settings.master_thermostat; + root["shower_timer"] = settings.shower_timer; + root["shower_alert"] = settings.shower_alert; } StateUpdateResult EMSESPSettings::update(JsonObject & root, EMSESPSettings & settings) { - EMSESPSettings newSettings = {}; - newSettings.tx_mode = root["tx_mode"] | EMSESP_DEFAULT_TX_MODE; - newSettings.ems_bus_id = root["ems_bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID; + settings.tx_mode = root["tx_mode"] | EMSESP_DEFAULT_TX_MODE; + settings.ems_bus_id = root["ems_bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID; + settings.syslog_level = root["syslog_level"] | EMSESP_DEFAULT_SYSLOG_LEVEL; + settings.syslog_mark_interval = root["syslog_mark_interval"] | EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL; + settings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST; + settings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT; + settings.shower_timer = root["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER; + settings.shower_alert = root["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT; - newSettings.syslog_level = root["syslog_level"] | EMSESP_DEFAULT_SYSLOG_LEVEL; - newSettings.syslog_mark_interval = root["syslog_mark_interval"] | EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL; - newSettings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST; + return StateUpdateResult::CHANGED; +} - newSettings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT; - newSettings.shower_timer = root["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER; - newSettings.shower_alert = root["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT; - - bool changed = false; - - if (newSettings.tx_mode != settings.tx_mode) { - EMSESP::reset_tx(newSettings.tx_mode); // reset counters - changed = true; - } - - if ((newSettings.shower_timer != settings.shower_timer) || (newSettings.shower_alert != settings.shower_alert)) { - EMSESP::shower_.start(); - changed = true; - } - - if ((newSettings.syslog_level != settings.syslog_level) || (newSettings.syslog_mark_interval != settings.syslog_mark_interval) - || !newSettings.syslog_host.equals(settings.syslog_host)) { - EMSESP::system_.syslog_init(); - changed = true; - } - - if (changed) { - settings = newSettings; - return StateUpdateResult::CHANGED; - } - - return StateUpdateResult::UNCHANGED; +// this is called after the settings have been persisted to the filesystem +void EMSESPSettingsService::onUpdate() { + EMSESP::shower_.start(); + EMSESP::system_.syslog_init(); + EMSESP::reset_tx(); } void EMSESPSettingsService::begin() { diff --git a/src/EMSESPSettingsService.h b/src/EMSESPSettingsService.h index 8e9ff8e4e..4bbd22f4c 100644 --- a/src/EMSESPSettingsService.h +++ b/src/EMSESPSettingsService.h @@ -67,6 +67,8 @@ class EMSESPSettingsService : public StatefulService { private: HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; + + void onUpdate(); }; } // namespace emsesp From b1c92b6f14450e6483cd6c106ffbb1608eb678cf Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 11:53:19 +0200 Subject: [PATCH 39/66] send warning to console/syslog if master boiler is restarted --- src/devices/boiler.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index acac09422..7885fe518 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -691,13 +691,6 @@ void Boiler::process_UBAMaintenanceStatus(std::shared_ptr telegr // first byte: Maintenance due (0 = no, 3 = yes, due to operating hours, 8 = yes, due to date) } -// 0x15 -// not yet implemented -void Boiler::process_UBAMaintenanceData(std::shared_ptr telegram) { - // first byte: Maintenance messages (0 = none, 1 = by operating hours, 2 = by date) - // I see a value of 3 when the boiler is booted, so probably a flag -} - // 0x10, 0x11, 0x12 // not yet implemented void Boiler::process_UBAErrorMessage(std::shared_ptr telegram) { @@ -706,6 +699,15 @@ void Boiler::process_UBAErrorMessage(std::shared_ptr telegram) { #pragma GCC diagnostic pop +// 0x15 +void Boiler::process_UBAMaintenanceData(std::shared_ptr telegram) { + // first byte: Maintenance messages (0 = none, 1 = by operating hours, 2 = by date) + // I see a value of 3 in the 1st byte when the boiler is booted, so probably a flag + if (telegram->message_data[0] == 3) { + LOG_WARNING(F("Boiler has booted.")); + } +} + // Set the warm water temperature 0x33 void Boiler::set_warmwater_temp(const uint8_t temperature) { LOG_INFO(F("Setting boiler warm water temperature to %d C"), temperature); From 53bfbcee06bb4514360e2a13d002b03d8b7cc2ee Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 11:53:48 +0200 Subject: [PATCH 40/66] minor cleanups --- src/system.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index 45dd90758..c842f7c22 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -197,18 +197,16 @@ void System::syslog_init() { syslog_mark_interval_ = settings.syslog_mark_interval; syslog_host_ = settings.syslog_host; }); + EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { syslog_.hostname(wifiSettings.hostname.c_str()); }); #ifndef EMSESP_STANDALONE - syslog_.start(); // syslog service + syslog_.start(); // syslog service re-start // configure syslog IPAddress addr; - if (!addr.fromString(syslog_host_.c_str())) { addr = (uint32_t)0; } - - EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { syslog_.hostname(wifiSettings.hostname.c_str()); }); syslog_.log_level((uuid::log::Level)syslog_level_); syslog_.mark_interval(syslog_mark_interval_); syslog_.destination(addr); @@ -230,20 +228,20 @@ void System::start() { #endif } - // fetch settings - std::string hostname; - EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode_ = settings.tx_mode; }); + // fetch system heartbeat EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { system_heartbeat_ = settings.system_heartbeat; }); + + // print boot message EMSESP::esp8266React.getWiFiSettingsService()->read( [&](WiFiSettings & wifiSettings) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), wifiSettings.hostname.c_str(), EMSESP_APP_VERSION); }); - syslog_.log_level((uuid::log::Level)syslog_level_); syslog_init(); // init SysLog if (LED_GPIO) { pinMode(LED_GPIO, OUTPUT); // LED pin, 0 means disabled } + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode_ = settings.tx_mode; }); #ifndef EMSESP_FORCE_SERIAL EMSuart::start(tx_mode_); // start UART #endif From 9b9c7df1d817f580630e383f70371ac040eaa468 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 11:54:14 +0200 Subject: [PATCH 41/66] bump to b9 --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index ebc93e297..96637ec81 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "2.0.0b8" +#define EMSESP_APP_VERSION "2.0.0b9" From 4175fe56fd5117fde451ea3c68eaec0b6630c6b8 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 17:18:42 +0200 Subject: [PATCH 42/66] fix standalone (use make run) --- lib_standalone/Arduino.cpp | 69 +- lib_standalone/Arduino.h | 32 - lib_standalone/AsyncMqttClient.h | 204 ++--- lib_standalone/AsyncTCP.h | 193 +---- lib_standalone/ESP8266React.h | 50 +- lib_standalone/ESPAsyncWebServer.h | 415 +--------- lib_standalone/SecuritySettingsService.cpp | 141 ++++ lib_standalone/SecuritySettingsService.h | 116 +++ lib_standalone/WString.cpp | 907 ++------------------- lib_standalone/WString.h | 285 ++----- src/EMSESPDevicesService.cpp | 2 + src/system.cpp | 8 +- src/test/test.cpp | 2 +- 13 files changed, 520 insertions(+), 1904 deletions(-) create mode 100644 lib_standalone/SecuritySettingsService.cpp create mode 100644 lib_standalone/SecuritySettingsService.h diff --git a/lib_standalone/Arduino.cpp b/lib_standalone/Arduino.cpp index b2bf854ac..4e25c2cc6 100644 --- a/lib_standalone/Arduino.cpp +++ b/lib_standalone/Arduino.cpp @@ -37,6 +37,7 @@ static bool __output_pins[256]; static int __output_level[256]; int main(int argc __attribute__((unused)), char * argv[] __attribute__((unused))) { + memset(__output_pins, 0, sizeof(__output_pins)); memset(__output_level, 0, sizeof(__output_level)); @@ -111,71 +112,3 @@ int digitalRead(uint8_t pin) { return LOW; } } - - - - -/* - * Copy string src to buffer dst of size dsize. At most dsize-1 - * chars will be copied. Always NUL terminates (unless dsize == 0). - * Returns strlen(src); if retval >= dsize, truncation occurred. - * - * https://github.com/freebsd/freebsd/blob/master/sys/libkern/strlcpy.c - */ -size_t strlcpy(char * __restrict dst, const char * __restrict src, size_t dsize) { - const char * osrc = src; - size_t nleft = dsize; - - /* Copy as many bytes as will fit. */ - if (nleft != 0) { - while (--nleft != 0) { - if ((*dst++ = *src++) == '\0') - break; - } - } - - /* Not enough room in dst, add NUL and traverse rest of src. */ - if (nleft == 0) { - if (dsize != 0) - *dst = '\0'; /* NUL-terminate dst */ - while (*src++) - ; - } - - return (src - osrc - 1); /* count does not include NUL */ -} - -/* - * Appends src to string dst of size siz (unlike strncat, siz is the - * full size of dst, not space left). At most siz-1 characters - * will be copied. Always NUL terminates (unless siz <= strlen(dst)). - * Returns strlen(src) + MIN(siz, strlen(initial dst)). - * If retval >= siz, truncation occurred. - * - * https://github.com/freebsd/freebsd/blob/master/sys/libkern/strlcat.c - */ -size_t strlcat(char * dst, const char * src, size_t siz) { - char * d = dst; - const char * s = src; - size_t n = siz; - size_t dlen; - - /* Find the end of dst and adjust bytes left but don't go past end */ - while (n-- != 0 && *d != '\0') - d++; - dlen = d - dst; - n = siz - dlen; - - if (n == 0) - return (dlen + strlen(s)); - while (*s != '\0') { - if (n != 1) { - *d++ = *s; - n--; - } - s++; - } - *d = '\0'; - - return (dlen + (s - src)); /* count does not include NUL */ -} diff --git a/lib_standalone/Arduino.h b/lib_standalone/Arduino.h index 459fba0d8..fbefdc5f7 100644 --- a/lib_standalone/Arduino.h +++ b/lib_standalone/Arduino.h @@ -136,34 +136,6 @@ class Stream : public Print { virtual int peek() = 0; }; -/* -class String { - public: - String(const char * data = "") - : data_(data) { - } - - long toInt() const { - return std::stol(data_); - } - - const char * c_str() const { - return data_.c_str(); - } - - bool equals(String comp) { - return (data_ == comp.c_str()); - } - - bool isEmpty() { - return data_.empty(); - } - - private: - std::string data_; -}; -*/ - class NativeConsole : public Stream { public: void begin(unsigned long baud __attribute__((unused))) { @@ -225,10 +197,6 @@ void yield(void); void setup(void); void loop(void); - -size_t strlcpy(char * __restrict dst, const char * __restrict src, size_t dsize); -size_t strlcat(char * dst, const char * src, size_t siz); - #include "WString.h" #endif diff --git a/lib_standalone/AsyncMqttClient.h b/lib_standalone/AsyncMqttClient.h index 60751f0f9..73a1058ea 100644 --- a/lib_standalone/AsyncMqttClient.h +++ b/lib_standalone/AsyncMqttClient.h @@ -5,139 +5,115 @@ #include enum class AsyncMqttClientDisconnectReason : int8_t { - TCP_DISCONNECTED = 0, + TCP_DISCONNECTED = 0, - MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, - MQTT_IDENTIFIER_REJECTED = 2, - MQTT_SERVER_UNAVAILABLE = 3, - MQTT_MALFORMED_CREDENTIALS = 4, - MQTT_NOT_AUTHORIZED = 5, + MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, + MQTT_IDENTIFIER_REJECTED = 2, + MQTT_SERVER_UNAVAILABLE = 3, + MQTT_MALFORMED_CREDENTIALS = 4, + MQTT_NOT_AUTHORIZED = 5, - ESP8266_NOT_ENOUGH_SPACE = 6, + ESP8266_NOT_ENOUGH_SPACE = 6, - TLS_BAD_FINGERPRINT = 7 + TLS_BAD_FINGERPRINT = 7 }; struct AsyncMqttClientMessageProperties { - uint8_t qos; - bool dup; - bool retain; + uint8_t qos; + bool dup; + bool retain; }; namespace AsyncMqttClientInternals { -// user callbacks -typedef std::function OnConnectUserCallback; + +typedef std::function OnConnectUserCallback; typedef std::function OnDisconnectUserCallback; -typedef std::function OnSubscribeUserCallback; -typedef std::function OnUnsubscribeUserCallback; -typedef std::function OnMessageUserCallback; +typedef std::function OnSubscribeUserCallback; +typedef std::function OnUnsubscribeUserCallback; +typedef std::function OnMessageUserCallback; typedef std::function OnPublishUserCallback; -}; +}; // namespace AsyncMqttClientInternals class AsyncMqttClient { - public: - AsyncMqttClient(); - ~AsyncMqttClient(); + public: + AsyncMqttClient(); + ~AsyncMqttClient(); - AsyncMqttClient& setKeepAlive(uint16_t keepAlive); - AsyncMqttClient& setClientId(const char* clientId); - AsyncMqttClient& setCleanSession(bool cleanSession); - AsyncMqttClient& setMaxTopicLength(uint16_t maxTopicLength); - AsyncMqttClient& setCredentials(const char* username, const char* password = nullptr); - AsyncMqttClient& setWill(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0) { return *this; } - AsyncMqttClient& setServer(IPAddress ip, uint16_t port); - AsyncMqttClient& setServer(const char* host, uint16_t port); + AsyncMqttClient & setKeepAlive(uint16_t keepAlive); + AsyncMqttClient & setClientId(const char * clientId); + AsyncMqttClient & setCleanSession(bool cleanSession); + AsyncMqttClient & setMaxTopicLength(uint16_t maxTopicLength); + AsyncMqttClient & setCredentials(const char * username, const char * password = nullptr); + AsyncMqttClient & setWill(const char * topic, uint8_t qos, bool retain, const char * payload = nullptr, size_t length = 0) { + return *this; + } + AsyncMqttClient & setServer(IPAddress ip, uint16_t port); + AsyncMqttClient & setServer(const char * host, uint16_t port); - AsyncMqttClient& onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback) { return *this; } - AsyncMqttClient& onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback) { return *this; } - AsyncMqttClient& onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback) { return *this; } - AsyncMqttClient& onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback) { return *this; } - AsyncMqttClient& onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback) { return *this; } - AsyncMqttClient& onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback) { return *this; } + AsyncMqttClient & onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback) { + return *this; + } + AsyncMqttClient & onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback) { + return *this; + } + AsyncMqttClient & onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback) { + return *this; + } + AsyncMqttClient & onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback) { + return *this; + } + AsyncMqttClient & onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback) { + return *this; + } + AsyncMqttClient & onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback) { + return *this; + } - bool connected() const { return false; } - void connect() {} - void disconnect(bool force = false) {} - uint16_t subscribe(const char* topic, uint8_t qos) {return 0;} - uint16_t unsubscribe(const char* topic) {return 0;} - uint16_t publish(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0, bool dup = false, uint16_t message_id = 0) {return 0;} + bool connected() const { + return false; + } + void connect() { + } + void disconnect(bool force = false) { + } + uint16_t subscribe(const char * topic, uint8_t qos) { + return 0; + } + uint16_t unsubscribe(const char * topic) { + return 0; + } + uint16_t publish(const char * topic, uint8_t qos, bool retain, const char * payload = nullptr, size_t length = 0, bool dup = false, uint16_t message_id = 0) { + return 0; + } - const char* getClientId() {return "12";} + const char * getClientId() { + return "12"; + } - private: -// AsyncClient _client; - - bool _connected; - bool _connectPacketNotEnoughSpace; - bool _disconnectOnPoll; - bool _tlsBadFingerprint; - uint32_t _lastClientActivity; - uint32_t _lastServerActivity; - uint32_t _lastPingRequestTime; - char _generatedClientId[18 + 1]; // esp8266-abc123 and esp32-abcdef123456 - IPAddress _ip; - const char* _host; - bool _useIp; - uint16_t _port; - uint16_t _keepAlive; - bool _cleanSession; - const char* _clientId; - const char* _username; - const char* _password; - const char* _willTopic; - const char* _willPayload; - uint16_t _willPayloadLength; - uint8_t _willQos; - bool _willRetain; - -// std::vector _onConnectUserCallbacks; -// std::vector _onDisconnectUserCallbacks; -// std::vector _onSubscribeUserCallbacks; -// std::vector _onUnsubscribeUserCallbacks; -// std::vector _onMessageUserCallbacks; -// std::vector _onPublishUserCallbacks; - -// AsyncMqttClientInternals::ParsingInformation _parsingInformation; -// AsyncMqttClientInternals::Packet* _currentParsedPacket; -// uint8_t _remainingLengthBufferPosition; -// char _remainingLengthBuffer[4]; - -// uint16_t _nextPacketId; - -// std::vector _pendingPubRels; - -// std::vector _toSendAcks; - -// void _clear(); -// void _freeCurrentParsedPacket(); - - // TCP -// void _onConnect(AsyncClient* client); -// void _onDisconnect(AsyncClient* client); -// static void _onError(AsyncClient* client, int8_t error); -// void _onTimeout(AsyncClient* client, uint32_t time); -// static void _onAck(AsyncClient* client, size_t len, uint32_t time); -// void _onData(AsyncClient* client, char* data, size_t len); -// void _onPoll(AsyncClient* client); - -// // MQTT -// void _onPingResp(); -// void _onConnAck(bool sessionPresent, uint8_t connectReturnCode); -// void _onSubAck(uint16_t packetId, char status); -// void _onUnsubAck(uint16_t packetId); -// void _onMessage(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId); -// void _onPublish(uint16_t packetId, uint8_t qos); -// void _onPubRel(uint16_t packetId); -// void _onPubAck(uint16_t packetId); -// void _onPubRec(uint16_t packetId); -// void _onPubComp(uint16_t packetId); - -// bool _sendPing(); -// void _sendAcks(); -// bool _sendDisconnect(); - -// uint16_t _getNextPacketId(); + private: + bool _connected; + bool _connectPacketNotEnoughSpace; + bool _disconnectOnPoll; + bool _tlsBadFingerprint; + uint32_t _lastClientActivity; + uint32_t _lastServerActivity; + uint32_t _lastPingRequestTime; + char _generatedClientId[18 + 1]; // esp8266-abc123 and esp32-abcdef123456 + IPAddress _ip; + const char * _host; + bool _useIp; + uint16_t _port; + uint16_t _keepAlive; + bool _cleanSession; + const char * _clientId; + const char * _username; + const char * _password; + const char * _willTopic; + const char * _willPayload; + uint16_t _willPayloadLength; + uint8_t _willQos; + bool _willRetain; }; #endif \ No newline at end of file diff --git a/lib_standalone/AsyncTCP.h b/lib_standalone/AsyncTCP.h index 3b82e964d..c06bbd0a5 100644 --- a/lib_standalone/AsyncTCP.h +++ b/lib_standalone/AsyncTCP.h @@ -1,212 +1,29 @@ -/* - Asynchronous TCP library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - #ifndef ASYNCTCP_H_ #define ASYNCTCP_H_ #include "Arduino.h" #include -//If core is not defined, then we are running in Arduino or PIO -#ifndef CONFIG_ASYNC_TCP_RUNNING_CORE -#define CONFIG_ASYNC_TCP_RUNNING_CORE -1 //any available core -#define CONFIG_ASYNC_TCP_USE_WDT 1 //if enabled, adds between 33us and 200us per event -#endif - class AsyncClient; -#define ASYNC_MAX_ACK_TIME 5000 -#define ASYNC_WRITE_FLAG_COPY 0x01 //will allocate new buffer to hold the data while sending (else will hold reference to the data given) -#define ASYNC_WRITE_FLAG_MORE 0x02 //will not send PSH flag, meaning that there should be more data to be sent before the application should react. - -typedef std::function AcConnectHandler; -typedef std::function AcAckHandler; -typedef std::function AcErrorHandler; -typedef std::function AcDataHandler; -typedef std::function AcPacketHandler; -typedef std::function AcTimeoutHandler; - struct tcp_pcb; struct ip_addr; class AsyncClient { public: - AsyncClient(tcp_pcb* pcb = 0); + AsyncClient(tcp_pcb * pcb = 0); ~AsyncClient(); - - AsyncClient & operator=(const AsyncClient &other); - AsyncClient & operator+=(const AsyncClient &other); - - bool operator==(const AsyncClient &other); - - bool operator!=(const AsyncClient &other) { - return !(*this == other); - } - bool connect(IPAddress ip, uint16_t port); - bool connect(const char* host, uint16_t port); - void close(bool now = false); - void stop(); - int8_t abort(); - bool free(); - - bool canSend();//ack is not pending - size_t space();//space available in the TCP window - size_t add(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY);//add for sending - bool send();//send all data added with the method above - - //write equals add()+send() - size_t write(const char* data); - size_t write(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY); //only when canSend() == true - - uint8_t state(); - bool connecting(); - bool connected(); - bool disconnecting(); - bool disconnected(); - bool freeable();//disconnected or disconnecting - - uint16_t getMss(); - - uint32_t getRxTimeout(); - void setRxTimeout(uint32_t timeout);//no RX data timeout for the connection in seconds - - uint32_t getAckTimeout(); - void setAckTimeout(uint32_t timeout);//no ACK timeout for the last sent packet in milliseconds - - void setNoDelay(bool nodelay); - bool getNoDelay(); - - uint32_t getRemoteAddress(); - uint16_t getRemotePort(); - uint32_t getLocalAddress(); - uint16_t getLocalPort(); - - //compatibility - IPAddress remoteIP(); - uint16_t remotePort(); - IPAddress localIP(); - uint16_t localPort(); - - void onConnect(AcConnectHandler cb, void* arg = 0); //on successful connect - void onDisconnect(AcConnectHandler cb, void* arg = 0); //disconnected - void onAck(AcAckHandler cb, void* arg = 0); //ack received - void onError(AcErrorHandler cb, void* arg = 0); //unsuccessful connect or error - void onData(AcDataHandler cb, void* arg = 0); //data received (called if onPacket is not used) - void onPacket(AcPacketHandler cb, void* arg = 0); //data received - void onTimeout(AcTimeoutHandler cb, void* arg = 0); //ack timeout - void onPoll(AcConnectHandler cb, void* arg = 0); //every 125ms when connected - - void ackPacket(struct pbuf * pb);//ack pbuf from onPacket - size_t ack(size_t len); //ack data that you have not acked using the method below - void ackLater(){ _ack_pcb = false; } //will not ack the current packet. Call from onData - - const char * errorToString(int8_t error); - const char * stateToString(); - - //Do not use any of the functions below! - static int8_t _s_poll(void *arg, struct tcp_pcb *tpcb); - static int8_t _s_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *pb, int8_t err); - static int8_t _s_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); - static int8_t _s_lwip_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); - static void _s_error(void *arg, int8_t err); - static int8_t _s_sent(void *arg, struct tcp_pcb *tpcb, uint16_t len); - static int8_t _s_connected(void* arg, void* tpcb, int8_t err); - static void _s_dns_found(const char *name, struct ip_addr *ipaddr, void *arg); - - int8_t _recv(tcp_pcb* pcb, pbuf* pb, int8_t err); - tcp_pcb * pcb(){ return _pcb; } - - protected: - tcp_pcb* _pcb; - int8_t _closed_slot; - - AcConnectHandler _connect_cb; - void* _connect_cb_arg; - AcConnectHandler _discard_cb; - void* _discard_cb_arg; - AcAckHandler _sent_cb; - void* _sent_cb_arg; - AcErrorHandler _error_cb; - void* _error_cb_arg; - AcDataHandler _recv_cb; - void* _recv_cb_arg; - AcPacketHandler _pb_cb; - void* _pb_cb_arg; - AcTimeoutHandler _timeout_cb; - void* _timeout_cb_arg; - AcConnectHandler _poll_cb; - void* _poll_cb_arg; - - bool _pcb_busy; - uint32_t _pcb_sent_at; - bool _ack_pcb; - uint32_t _rx_ack_len; - uint32_t _rx_last_packet; - uint32_t _rx_since_timeout; - uint32_t _ack_timeout; - uint16_t _connect_port; - - int8_t _close(); - int8_t _connected(void* pcb, int8_t err); - void _error(int8_t err); - int8_t _poll(tcp_pcb* pcb); - int8_t _sent(tcp_pcb* pcb, uint16_t len); - int8_t _fin(tcp_pcb* pcb, int8_t err); - int8_t _lwip_fin(tcp_pcb* pcb, int8_t err); - void _dns_found(struct ip_addr *ipaddr); - - public: - AsyncClient* prev; - AsyncClient* next; }; class AsyncServer { public: - AsyncServer(IPAddress addr, uint16_t port) : _port(port), _addr("poep") {}; - - AsyncServer(uint16_t port) : _port(port) {}; - - ~AsyncServer() {}; - void onClient(AcConnectHandler cb, void* arg); - void begin(); - void end(); - void setNoDelay(bool nodelay); - bool getNoDelay(); - uint8_t status(); - - //Do not use any of the functions below! - static int8_t _s_accept(void *arg, tcp_pcb* newpcb, int8_t err); - static int8_t _s_accepted(void *arg, AsyncClient* client); + AsyncServer(uint16_t port) + : _port(port){}; + ~AsyncServer(){}; protected: uint16_t _port; - IPAddress _addr; - bool _noDelay; - tcp_pcb* _pcb; - AcConnectHandler _connect_cb; - void* _connect_cb_arg; - - int8_t _accept(tcp_pcb* newpcb, int8_t err); - int8_t _accepted(AsyncClient* client); }; -#endif /* ASYNCTCP_H_ */ +#endif diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index 7c40f4ea5..718a32b67 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -4,32 +4,31 @@ #include #include #include -#include +#include #include #include -#include +#include +#include #include class DummySettings { public: - uint8_t tx_mode; - uint8_t ems_bus_id; - bool system_heartbeat; - int8_t syslog_level; // uuid::log::Level - uint32_t syslog_mark_interval; - String syslog_host; - uint8_t master_thermostat; - bool shower_timer; - bool shower_alert; - - uint16_t publish_time; // seconds - uint8_t mqtt_format; // 1=single, 2=nested, 3=ha, 4=custom - uint8_t mqtt_qos; - - String hostname; - String jwtSecret; - String ssid; - String password; + uint8_t tx_mode = 1; + uint8_t ems_bus_id = 0x0B; + bool system_heartbeat = false; + int8_t syslog_level = 1; // uuid::log::Level + uint32_t syslog_mark_interval = 0; + String syslog_host = "192.168.1.4"; + uint8_t master_thermostat = 0; + bool shower_timer = false; + bool shower_alert = false; + uint16_t publish_time = 10; // seconds + uint8_t mqtt_format = 1; // 1=single, 2=nested, 3=ha, 4=custom + uint8_t mqtt_qos = 0; + String hostname = "ems-esp"; + String jwtSecret = "ems-esp"; + String ssid = "ems-esp"; + String password = "ems-esp"; static void read(DummySettings & settings, JsonObject & root){}; static void read(DummySettings & settings){}; @@ -56,13 +55,14 @@ class DummySettingsService : public StatefulService { class ESP8266React { public: ESP8266React(AsyncWebServer * server, FS * fs) - : _settings(server, fs, nullptr){}; + : _settings(server, fs, nullptr) + , _securitySettingsService(server, fs){}; void begin(){}; void loop(){}; SecurityManager * getSecurityManager() { - return nullptr; + return &_securitySettingsService; } AsyncMqttClient * getMqttClient() { @@ -82,8 +82,10 @@ class ESP8266React { } private: - DummySettingsService _settings; - AsyncMqttClient * _mqttClient; + DummySettingsService _settings; + SecuritySettingsService _securitySettingsService; + + AsyncMqttClient * _mqttClient; }; class EMSESPSettingsService { diff --git a/lib_standalone/ESPAsyncWebServer.h b/lib_standalone/ESPAsyncWebServer.h index 5a0ce867b..35a848485 100644 --- a/lib_standalone/ESPAsyncWebServer.h +++ b/lib_standalone/ESPAsyncWebServer.h @@ -7,22 +7,11 @@ #include #include -#define DEBUGF(...) //Serial.printf(__VA_ARGS__) - class AsyncWebServer; class AsyncWebServerRequest; class AsyncWebServerResponse; -class AsyncWebHeader; -class AsyncWebParameter; -class AsyncWebRewrite; -class AsyncWebHandler; -class AsyncStaticWebHandler; -class AsyncCallbackWebHandler; -class AsyncResponseStream; class AsyncJsonResponse; - - typedef enum { HTTP_GET = 0b00000001, HTTP_POST = 0b00000010, @@ -34,151 +23,17 @@ typedef enum { HTTP_ANY = 0b01111111, } WebRequestMethod; -//if this value is returned when asked for data, packet will not be sent and you will be asked for data again -#define RESPONSE_TRY_AGAIN 0xFFFFFFFF - typedef uint8_t WebRequestMethodComposite; typedef std::function ArDisconnectHandler; -/* - * PARAMETER :: Chainable object to hold GET/POST and FILE parameters - * */ - -class AsyncWebParameter { - private: - String _name; - String _value; - size_t _size; - bool _isForm; - bool _isFile; - - public: - AsyncWebParameter(const String & name, const String & value, bool form = false, bool file = false, size_t size = 0) - : _name(name) - , _value(value) - , _size(size) - , _isForm(form) - , _isFile(file) { - } - const String & name() const { - return _name; - } - const String & value() const { - return _value; - } - size_t size() const { - return _size; - } - bool isPost() const { - return _isForm; - } - bool isFile() const { - return _isFile; - } -}; - -/* - * HEADER :: Chainable object to hold the headers - * */ - -class AsyncWebHeader { - private: - String _name; - String _value; - - public: - AsyncWebHeader(const String & name, const String & value) - : _name(name) - , _value(value) { - } - AsyncWebHeader(const String & data) - : _name() - , _value() { - } - ~AsyncWebHeader() { - } - const String & name() const { - return _name; - } - const String & value() const { - return _value; - } - String toString() const { - return _value; - } -}; - -/* - * REQUEST :: Each incoming Client is wrapped inside a Request and both live together until disconnect - * */ - -typedef enum { RCT_NOT_USED = -1, RCT_DEFAULT = 0, RCT_HTTP, RCT_WS, RCT_EVENT, RCT_MAX } RequestedConnectionType; - -typedef std::function AwsResponseFiller; -typedef std::function AwsTemplateProcessor; - class AsyncWebServerRequest { friend class AsyncWebServer; friend class AsyncCallbackWebHandler; private: - AsyncClient * _client; - AsyncWebServer * _server; - AsyncWebHandler * _handler; - AsyncWebServerResponse * _response; - ArDisconnectHandler _onDisconnectfn; - - String _temp; - uint8_t _parseState; - - uint8_t _version; + AsyncClient * _client; + AsyncWebServer * _server; WebRequestMethodComposite _method; - String _url; - String _host; - String _contentType; - String _boundary; - String _authorization; - RequestedConnectionType _reqconntype; - void _removeNotInterestingHeaders(); - bool _isDigest; - bool _isMultipart; - bool _isPlainPost; - bool _expectingContinue; - size_t _contentLength; - size_t _parsedLength; - - uint8_t _multiParseState; - uint8_t _boundaryPosition; - size_t _itemStartIndex; - size_t _itemSize; - String _itemName; - String _itemFilename; - String _itemType; - String _itemValue; - uint8_t * _itemBuffer; - size_t _itemBufferIndex; - bool _itemIsFile; - - void _onPoll(); - void _onAck(size_t len, uint32_t time); - void _onError(int8_t error); - void _onTimeout(uint32_t time); - void _onDisconnect(){}; - void _onData(void * buf, size_t len); - - void _addParam(AsyncWebParameter *); - void _addPathParam(const char * param); - - bool _parseReqHead(); - bool _parseReqHeader(); - void _parseLine(); - void _parsePlainPostChar(uint8_t data); - void _parseMultipartPostByte(uint8_t data, bool last); - void _addGetParams(const String & params); - - void _handleUploadStart(); - void _handleUploadByte(uint8_t data, bool last); - void _handleUploadEnd(); public: void * _tempObject; @@ -189,157 +44,30 @@ class AsyncWebServerRequest { AsyncClient * client() { return _client; } - uint8_t version() const { - return _version; - } + WebRequestMethodComposite method() const { return _method; } - const String & url() const { - return _url; - } - const String & host() const { - return _host; - } - const String & contentType() const { - return _contentType; - } - size_t contentLength() const { - return _contentLength; - } - bool multipart() const { - return _isMultipart; - } - const char * methodToString() const; - const char * requestedConnTypeToString() const; - RequestedConnectionType requestedConnType() const { - return _reqconntype; - } - bool isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2 = RCT_NOT_USED, RequestedConnectionType erct3 = RCT_NOT_USED); - void onDisconnect(ArDisconnectHandler fn){}; - //hash is the string representation of: - // base64(user:pass) for basic or - // user:realm:md5(user:realm:pass) for digest - bool authenticate(const char * hash); - bool authenticate(const char * username, const char * password, const char * realm = NULL, bool passwordIsHash = false); - void requestAuthentication(const char * realm = NULL, bool isDigest = true); - - void setHandler(AsyncWebHandler * handler) { - _handler = handler; - } - void addInterestingHeader(const String & name); - - void redirect(const String & url); + void addInterestingHeader(const String & name){}; void send(AsyncWebServerResponse * response){}; void send(AsyncJsonResponse * response){}; void send(int code, const String & contentType = String(), const String & content = String()){}; - void send(Stream & stream, const String & contentType, size_t len, AwsTemplateProcessor callback = nullptr); - void send(const String & contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - void sendChunked(const String & contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - void send_P(int code, const String & contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback = nullptr); - void send_P(int code, const String & contentType, PGM_P content, AwsTemplateProcessor callback = nullptr); + AsyncWebServerResponse * beginResponse(int code, const String & contentType = String(), const String & content = String()) { + // AsyncWebServerResponse *a = new AsyncWebServerResponse() + return nullptr; + } - AsyncWebServerResponse * beginResponse(int code, const String & contentType = String(), const String & content = String()); - AsyncWebServerResponse * beginResponse(Stream & stream, const String & contentType, size_t len, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse * beginResponse(const String & contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - AsyncWebServerResponse * beginChunkedResponse(const String & contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - AsyncResponseStream * beginResponseStream(const String & contentType, size_t bufferSize = 1460); - AsyncWebServerResponse * beginResponse_P(int code, const String & contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse * beginResponse_P(int code, const String & contentType, PGM_P content, AwsTemplateProcessor callback = nullptr); - - size_t headers() const; // get header count - bool hasHeader(const String & name) const; // check if header exists - bool hasHeader(const __FlashStringHelper * data) const; // check if header exists - - AsyncWebHeader * getHeader(const String & name) const; - AsyncWebHeader * getHeader(const __FlashStringHelper * data) const; - AsyncWebHeader * getHeader(size_t num) const; - - size_t params() const; // get arguments count - bool hasParam(const String & name, bool post = false, bool file = false) const; - bool hasParam(const __FlashStringHelper * data, bool post = false, bool file = false) const; - - AsyncWebParameter * getParam(const String & name, bool post = false, bool file = false) const; - AsyncWebParameter * getParam(const __FlashStringHelper * data, bool post, bool file) const; - AsyncWebParameter * getParam(size_t num) const; - - size_t args() const { - return params(); - } // get arguments count - const String & arg(const String & name) const; // get request argument value by name - const String & arg(const __FlashStringHelper * data) const; // get request argument value by F(name) - const String & arg(size_t i) const; // get request argument value by number - const String & argName(size_t i) const; // get request argument name by number - bool hasArg(const char * name) const; // check if argument exists - bool hasArg(const __FlashStringHelper * data) const; // check if F(argument) exists - - const String & pathArg(size_t i) const; - - const String & header(const char * name) const; // get request header value by name - const String & header(const __FlashStringHelper * data) const; // get request header value by F(name) - const String & header(size_t i) const; // get request header value by number - const String & headerName(size_t i) const; // get request header name by number - String urlDecode(const String & text) const; + size_t headers() const; // get header count + size_t params() const; // get arguments count }; -/* - * FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server) - * */ - typedef std::function ArRequestFilterFunction; -bool ON_STA_FILTER(AsyncWebServerRequest * request); - -bool ON_AP_FILTER(AsyncWebServerRequest * request); - -/* - * REWRITE :: One instance can be handle any Request (done by the Server) - * */ - -class AsyncWebRewrite { - protected: - String _from; - String _toUrl; - String _params; - ArRequestFilterFunction _filter; - - public: - AsyncWebRewrite(const char * from, const char * to) - : _from(from) - , _toUrl(to) - , _params(String()) - , _filter(NULL) { - } - virtual ~AsyncWebRewrite() { - } - AsyncWebRewrite & setFilter(ArRequestFilterFunction fn) { - _filter = fn; - return *this; - } - bool filter(AsyncWebServerRequest * request) const { - return _filter == NULL || _filter(request); - } - const String & from(void) const { - return _from; - } - const String & toUrl(void) const { - return _toUrl; - } - const String & params(void) const { - return _params; - } -}; - -/* - * HANDLER :: One instance can be attached to any Request (done by the Server) - * */ - class AsyncWebHandler { protected: - ArRequestFilterFunction _filter; String _username; String _password; @@ -348,18 +76,7 @@ class AsyncWebHandler { : _username("") , _password("") { } - AsyncWebHandler & setFilter(ArRequestFilterFunction fn) { - _filter = fn; - return *this; - } - AsyncWebHandler & setAuthentication(const char * username, const char * password) { - _username = String(username); - _password = String(password); - return *this; - }; - bool filter(AsyncWebServerRequest * request) { - return _filter == NULL || _filter(request); - } + virtual ~AsyncWebHandler() { } virtual bool canHandle(AsyncWebServerRequest * request __attribute__((unused))) { @@ -380,142 +97,42 @@ class AsyncWebHandler { size_t index __attribute__((unused)), size_t total __attribute__((unused))) { } + virtual bool isRequestHandlerTrivial() { return true; } }; -/* - * RESPONSE :: One instance is created for each Request (attached by the Handler) - * */ - -typedef enum { RESPONSE_SETUP, RESPONSE_HEADERS, RESPONSE_CONTENT, RESPONSE_WAIT_ACK, RESPONSE_END, RESPONSE_FAILED } WebResponseState; - class AsyncWebServerResponse { - protected: - int _code; - String _contentType; - size_t _contentLength; - bool _sendContentLength; - bool _chunked; - size_t _headLength; - size_t _sentLength; - size_t _ackedLength; - size_t _writtenLength; - WebResponseState _state; - const char * _responseCodeToString(int code); - public: AsyncWebServerResponse(); virtual ~AsyncWebServerResponse(); - virtual void setCode(int code); - virtual void setContentLength(size_t len); - virtual void setContentType(const String & type); - virtual void addHeader(const String & name, const String & value); - virtual String _assembleHead(uint8_t version); - virtual bool _started() const; - virtual bool _finished() const; - virtual bool _failed() const; - virtual bool _sourceValid() const; - virtual void _respond(AsyncWebServerRequest * request); - virtual size_t _ack(AsyncWebServerRequest * request, size_t len, uint32_t time); }; -/* - * SERVER :: One instance - * */ - typedef std::function ArRequestHandlerFunction; typedef std::function ArUploadHandlerFunction; typedef std::function ArBodyHandlerFunction; class AsyncWebServer { protected: - AsyncServer _server; - AsyncCallbackWebHandler * _catchAllHandler; + AsyncServer _server; public: - // proddy AsyncWebServer(uint16_t port) : _server(port){}; - // , _rewrites(LinkedList([](AsyncWebRewrite* r){ delete r; })) - // , _handlers(LinkedList([](AsyncWebHandler* h){ delete h; })) - ~AsyncWebServer(){}; void begin(){}; void end(); -#if ASYNC_TCP_SSL_ENABLED - void onSslFileRequest(AcSSlFileHandler cb, void * arg); - void beginSecure(const char * cert, const char * private_key_file, const char * password); -#endif - - AsyncWebRewrite & addRewrite(AsyncWebRewrite * rewrite); - bool removeRewrite(AsyncWebRewrite * rewrite); - AsyncWebRewrite & rewrite(const char * from, const char * to); - - AsyncWebHandler & addHandler(AsyncWebHandler * handler); - bool removeHandler(AsyncWebHandler * handler); - - /* - - AsyncCallbackWebHandler & on(const char * uri, ArRequestHandlerFunction onRequest) { - AsyncCallbackWebHandler * handler = new AsyncCallbackWebHandler(); + AsyncWebHandler & addHandler(AsyncWebHandler * handler) { return *handler; - }; - AsyncCallbackWebHandler & on(const char * uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest) { - AsyncCallbackWebHandler * handler = new AsyncCallbackWebHandler(); - return handler; - }; - AsyncCallbackWebHandler & on(const char * uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload) { - AsyncCallbackWebHandler * handler = new AsyncCallbackWebHandler(); - return *handler; - }; - AsyncCallbackWebHandler & - on(const char * uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody) { - AsyncCallbackWebHandler * handler = new AsyncCallbackWebHandler(); - return *handler; - }; - */ + } void on(const char * uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest){}; - void onNotFound(ArRequestHandlerFunction fn); //called when handler is not assigned - void onFileUpload(ArUploadHandlerFunction fn); //handle file uploads - void onRequestBody(ArBodyHandlerFunction fn); //handle posts with plain body content (JSON often transmitted this way as a request) - - void reset(); //remove all writers and handlers, with onNotFound/onFileUpload/onRequestBody - - void _handleDisconnect(AsyncWebServerRequest * request); - void _attachHandler(AsyncWebServerRequest * request); - void _rewriteRequest(AsyncWebServerRequest * request); }; -// class DefaultHeaders { -// headers_t _headers; -// DefaultHeaders() -// public: - -// void addHeader(const String& name, const String& value){ -// _headers.add(new AsyncWebHeader(name, value)); -// } - -// DefaultHeaders(DefaultHeaders const &) = delete; -// DefaultHeaders &operator=(DefaultHeaders const &) = delete; -// static DefaultHeaders &Instance() { -// static DefaultHeaders instance; -// return instance; -// } -// }; - -// #include "WebResponseImpl.h" -// #include "WebHandlerImpl.h" -// #include "AsyncWebSocket.h" -// #include "AsyncEventSource.h" - -typedef std::function ArJsonRequestHandlerFunction; - -#endif /* _AsyncWebServer_H_ */ +#endif diff --git a/lib_standalone/SecuritySettingsService.cpp b/lib_standalone/SecuritySettingsService.cpp new file mode 100644 index 000000000..08ba401d8 --- /dev/null +++ b/lib_standalone/SecuritySettingsService.cpp @@ -0,0 +1,141 @@ +#include + +#if FT_ENABLED(FT_SECURITY) + +SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : + _httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this), + _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE), + _jwtHandler(FACTORY_JWT_SECRET) { + addUpdateHandler([&](const String& originId) { configureJWTHandler(); }, false); +} + +void SecuritySettingsService::begin() { + _fsPersistence.readFromFS(); + configureJWTHandler(); +} + +Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest* request) { + AsyncWebHeader* authorizationHeader = request->getHeader(AUTHORIZATION_HEADER); + if (authorizationHeader) { + String value = authorizationHeader->value(); + if (value.startsWith(AUTHORIZATION_HEADER_PREFIX)) { + value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN); + return authenticateJWT(value); + } + } else if (request->hasParam(ACCESS_TOKEN_PARAMATER)) { + AsyncWebParameter* tokenParamater = request->getParam(ACCESS_TOKEN_PARAMATER); + String value = tokenParamater->value(); + return authenticateJWT(value); + } + return Authentication(); +} + +void SecuritySettingsService::configureJWTHandler() { + _jwtHandler.setSecret(_state.jwtSecret); +} + +Authentication SecuritySettingsService::authenticateJWT(String& jwt) { + DynamicJsonDocument payloadDocument(MAX_JWT_SIZE); + _jwtHandler.parseJWT(jwt, payloadDocument); + if (payloadDocument.is()) { + JsonObject parsedPayload = payloadDocument.as(); + String username = parsedPayload["username"]; + for (User _user : _state.users) { + if (_user.username == username && validatePayload(parsedPayload, &_user)) { + return Authentication(_user); + } + } + } + return Authentication(); +} + +Authentication SecuritySettingsService::authenticate(const String& username, const String& password) { + for (User _user : _state.users) { + if (_user.username == username && _user.password == password) { + return Authentication(_user); + } + } + return Authentication(); +} + +inline void populateJWTPayload(JsonObject& payload, User* user) { + payload["username"] = user->username; + payload["admin"] = user->admin; + payload["version"] = EMSESP_APP_VERSION; // proddy added +} + +boolean SecuritySettingsService::validatePayload(JsonObject& parsedPayload, User* user) { + DynamicJsonDocument jsonDocument(MAX_JWT_SIZE); + JsonObject payload = jsonDocument.to(); + populateJWTPayload(payload, user); + return payload == parsedPayload; +} + +String SecuritySettingsService::generateJWT(User* user) { + DynamicJsonDocument jsonDocument(MAX_JWT_SIZE); + JsonObject payload = jsonDocument.to(); + populateJWTPayload(payload, user); + return _jwtHandler.buildJWT(payload); +} + +ArRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) { + return [this, predicate](AsyncWebServerRequest* request) { + Authentication authentication = authenticateRequest(request); + return predicate(authentication); + }; +} + +ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFunction onRequest, + AuthenticationPredicate predicate) { + return [this, onRequest, predicate](AsyncWebServerRequest* request) { + Authentication authentication = authenticateRequest(request); + if (!predicate(authentication)) { + request->send(401); + return; + } + onRequest(request); + }; +} + +ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction onRequest, + AuthenticationPredicate predicate) { + return [this, onRequest, predicate](AsyncWebServerRequest* request, JsonVariant& json) { + Authentication authentication = authenticateRequest(request); + if (!predicate(authentication)) { + request->send(401); + return; + } + onRequest(request, json); + }; +} + +#else + +User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true); + +SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : SecurityManager() { +} +SecuritySettingsService::~SecuritySettingsService() { +} + +ArRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) { + return [this, predicate](AsyncWebServerRequest* request) { return true; }; +} + +// Return the admin user on all request - disabling security features +Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest* request) { + return Authentication(ADMIN_USER); +} + +// Return the function unwrapped +ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFunction onRequest, + AuthenticationPredicate predicate) { + return onRequest; +} + +ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction onRequest, + AuthenticationPredicate predicate) { + return onRequest; +} + +#endif diff --git a/lib_standalone/SecuritySettingsService.h b/lib_standalone/SecuritySettingsService.h new file mode 100644 index 000000000..801e44a3e --- /dev/null +++ b/lib_standalone/SecuritySettingsService.h @@ -0,0 +1,116 @@ +#ifndef SecuritySettingsService_h +#define SecuritySettingsService_h + +#include +#include +#include +#include + +#include "../../src/version.h" // added by proddy + +#ifndef FACTORY_ADMIN_USERNAME +#define FACTORY_ADMIN_USERNAME "admin" +#endif + +#ifndef FACTORY_ADMIN_PASSWORD +#define FACTORY_ADMIN_PASSWORD "admin" +#endif + +#ifndef FACTORY_GUEST_USERNAME +#define FACTORY_GUEST_USERNAME "guest" +#endif + +#ifndef FACTORY_GUEST_PASSWORD +#define FACTORY_GUEST_PASSWORD "guest" +#endif + +#define SECURITY_SETTINGS_FILE "/config/securitySettings.json" +#define SECURITY_SETTINGS_PATH "/rest/securitySettings" + +#if FT_ENABLED(FT_SECURITY) + +class SecuritySettings { + public: + String jwtSecret; + std::list users; + + static void read(SecuritySettings& settings, JsonObject& root) { + // secret + root["jwt_secret"] = settings.jwtSecret; + + // users + JsonArray users = root.createNestedArray("users"); + for (User user : settings.users) { + JsonObject userRoot = users.createNestedObject(); + userRoot["username"] = user.username; + userRoot["password"] = user.password; + userRoot["admin"] = user.admin; + } + } + + static StateUpdateResult update(JsonObject& root, SecuritySettings& settings) { + // secret + settings.jwtSecret = root["jwt_secret"] | FACTORY_JWT_SECRET; + + // users + settings.users.clear(); + if (root["users"].is()) { + for (JsonVariant user : root["users"].as()) { + settings.users.push_back(User(user["username"], user["password"], user["admin"])); + } + } else { + settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true)); + settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false)); + } + return StateUpdateResult::CHANGED; + } +}; + +class SecuritySettingsService : public StatefulService, public SecurityManager { + public: + SecuritySettingsService(AsyncWebServer* server, FS* fs); + + void begin(); + + // Functions to implement SecurityManager + Authentication authenticate(const String& username, const String& password); + Authentication authenticateRequest(AsyncWebServerRequest* request); + String generateJWT(User* user); + ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); + ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); + ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate); + + private: + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; + ArduinoJsonJWT _jwtHandler; + + void configureJWTHandler(); + + /* + * Lookup the user by JWT + */ + Authentication authenticateJWT(String& jwt); + + /* + * Verify the payload is correct + */ + boolean validatePayload(JsonObject& parsedPayload, User* user); +}; + +#else + +class SecuritySettingsService : public SecurityManager { + public: + SecuritySettingsService(AsyncWebServer* server, FS* fs); + ~SecuritySettingsService(); + + // minimal set of functions to support framework with security settings disabled + Authentication authenticateRequest(AsyncWebServerRequest* request); + ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); + ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); + ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest, AuthenticationPredicate predicate); +}; + +#endif // end FT_ENABLED(FT_SECURITY) +#endif // end SecuritySettingsService_h diff --git a/lib_standalone/WString.cpp b/lib_standalone/WString.cpp index 5fd02b086..ef3e571cc 100644 --- a/lib_standalone/WString.cpp +++ b/lib_standalone/WString.cpp @@ -1,863 +1,68 @@ -/* - WString.cpp - String library for Wiring & Arduino - ...mostly rewritten by Paul Stoffregen... - Copyright (c) 2009-10 Hernando Barragan. All rights reserved. - Copyright 2011, Paul Stoffregen, paul@pjrc.com - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ +#include #include "WString.h" -/*********************************************/ -/* Constructors */ -/*********************************************/ +/* + * Copy string src to buffer dst of size dsize. At most dsize-1 + * chars will be copied. Always NUL terminates (unless dsize == 0). + * Returns strlen(src); if retval >= dsize, truncation occurred. + * + * https://github.com/freebsd/freebsd/blob/master/sys/libkern/strlcpy.c + */ +size_t strlcpy(char * __restrict dst, const char * __restrict src, size_t dsize) { + const char * osrc = src; + size_t nleft = dsize; -char * itoa(signed short value, char * result, const unsigned int base) { - // check that the base if valid - if (base < 2 || base > 36) { - *result = '\0'; - return result; - } - - char * ptr = result, *ptr1 = result, tmp_char; - signed short tmp_value; - - do { - tmp_value = value; - value /= base; - *ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz"[35 + (tmp_value - value * base)]; - } while (value); - - // Apply negative sign - if (tmp_value < 0) { - *ptr++ = '-'; - } - - *ptr-- = '\0'; - while (ptr1 < ptr) { - tmp_char = *ptr; - *ptr-- = *ptr1; - *ptr1++ = tmp_char; - } - - return result; -} - -char* ltoa(long value, char* result, int base) { - return itoa((int)value, result, base); -} - -char* ultoa(unsigned long value, char* result, int base) { - return itoa((unsigned int)value, result, base); -} - -char * dtostrf(double number, signed char width, unsigned char prec, char *s) { - bool negative = false; - - char* out = s; - - int fillme = width; // how many cells to fill for the integer part - if (prec > 0) { - fillme -= (prec+1); - } - - // Handle negative numbers - if (number < 0.0) { - negative = true; - fillme--; - number = -number; - } - - // Round correctly so that print(1.999, 2) prints as "2.00" - // I optimized out most of the divisions - double rounding = 2.0; - for (unsigned int i = 0; i < prec; ++i) - rounding *= 10.0; - rounding = 1.0 / rounding; - - number += rounding; - - // Figure out how big our number really is - double tenpow = 1.0; - int digitcount = 1; - double nextpow; - while (number >= (nextpow = (10.0 * tenpow))) { - tenpow = nextpow; - digitcount++; - } - - // minimal compensation for possible lack of precision (#7087 addition) - // number *= 1 + std::numeric_limits::epsilon(); - - number /= tenpow; - fillme -= digitcount; - - // Pad unused cells with spaces - while (fillme-- > 0) { - *out++ = ' '; - } - - // Handle negative sign - if (negative) *out++ = '-'; - - // Print the digits, and if necessary, the decimal point - digitcount += prec; - int8_t digit = 0; - while (digitcount-- > 0) { - digit = (int8_t)number; - if (digit > 9) digit = 9; // insurance - *out++ = (char)('0' | digit); - if ((digitcount == prec) && (prec > 0)) { - *out++ = '.'; + /* Copy as many bytes as will fit. */ + if (nleft != 0) { + while (--nleft != 0) { + if ((*dst++ = *src++) == '\0') + break; } - number -= digit; - number *= 10.0; } - // make sure the string is terminated - *out = 0; - return s; -} + /* Not enough room in dst, add NUL and traverse rest of src. */ + if (nleft == 0) { + if (dsize != 0) + *dst = '\0'; /* NUL-terminate dst */ + while (*src++) + ; + } -String::String(const char *cstr) -{ - init(); - if (cstr) copy(cstr, strlen(cstr)); -} - -String::String(const String &value) -{ - init(); - *this = value; -} - -String::String(const __FlashStringHelper *pstr) -{ - init(); - *this = pstr; -} - -#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__) -String::String(String &&rval) -{ - init(); - move(rval); -} -String::String(StringSumHelper &&rval) -{ - init(); - move(rval); -} -#endif - -String::String(char c) -{ - init(); - char buf[2]; - buf[0] = c; - buf[1] = 0; - *this = buf; -} - -String::String(unsigned char value, unsigned char base) -{ - init(); - char buf[1 + 8 * sizeof(unsigned char)]; - itoa(value, buf, base); - *this = buf; -} - -String::String(int value, unsigned char base) -{ - init(); - char buf[2 + 8 * sizeof(int)]; - itoa(value, buf, base); - *this = buf; -} - -String::String(unsigned int value, unsigned char base) -{ - init(); - char buf[1 + 8 * sizeof(unsigned int)]; - itoa(value, buf, base); - *this = buf; -} - -String::String(long value, unsigned char base) -{ - init(); - char buf[2 + 8 * sizeof(long)]; - ltoa(value, buf, base); - *this = buf; -} - -String::String(unsigned long value, unsigned char base) -{ - init(); - char buf[1 + 8 * sizeof(unsigned long)]; - ultoa(value, buf, base); - *this = buf; -} - -String::String(float value, unsigned char decimalPlaces) -{ - init(); - char buf[33]; - *this = dtostrf(value, (decimalPlaces + 2), decimalPlaces, buf); -} - -String::String(double value, unsigned char decimalPlaces) -{ - init(); - char buf[33]; - *this = dtostrf(value, (decimalPlaces + 2), decimalPlaces, buf); -} - -String::~String() -{ - free(buffer); -} - -/*********************************************/ -/* Memory Management */ -/*********************************************/ - -inline void String::init(void) -{ - buffer = NULL; - capacity = 0; - len = 0; -} - -void String::invalidate(void) -{ - if (buffer) free(buffer); - buffer = NULL; - capacity = len = 0; -} - -unsigned char String::reserve(unsigned int size) -{ - if (buffer && capacity >= size) return 1; - if (changeBuffer(size)) { - if (len == 0) buffer[0] = 0; - return 1; - } - return 0; -} - -unsigned char String::changeBuffer(unsigned int maxStrLen) -{ - char *newbuffer = (char *)realloc(buffer, maxStrLen + 1); - if (newbuffer) { - buffer = newbuffer; - capacity = maxStrLen; - return 1; - } - return 0; -} - -/*********************************************/ -/* Copy and Move */ -/*********************************************/ - -String & String::copy(const char *cstr, unsigned int length) -{ - if (!reserve(length)) { - invalidate(); - return *this; - } - len = length; - strcpy(buffer, cstr); - return *this; + return (src - osrc - 1); /* count does not include NUL */ } /* -String & String::copy(const char *pstr, unsigned int length) -{ - if (!reserve(length)) { - invalidate(); - return *this; - } - len = length; - strcpy_P(buffer, pstr); - return *this; + * Appends src to string dst of size siz (unlike strncat, siz is the + * full size of dst, not space left). At most siz-1 characters + * will be copied. Always NUL terminates (unless siz <= strlen(dst)). + * Returns strlen(src) + MIN(siz, strlen(initial dst)). + * If retval >= siz, truncation occurred. + * + * https://github.com/freebsd/freebsd/blob/master/sys/libkern/strlcat.c + */ +size_t strlcat(char * dst, const char * src, size_t siz) { + char * d = dst; + const char * s = src; + size_t n = siz; + size_t dlen; + + /* Find the end of dst and adjust bytes left but don't go past end */ + while (n-- != 0 && *d != '\0') + d++; + dlen = d - dst; + n = siz - dlen; + + if (n == 0) + return (dlen + strlen(s)); + while (*s != '\0') { + if (n != 1) { + *d++ = *s; + n--; + } + s++; + } + *d = '\0'; + + return (dlen + (s - src)); /* count does not include NUL */ } -*/ - -#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__) -void String::move(String &rhs) -{ - if (buffer) { - if (rhs && capacity >= rhs.len) { - strcpy(buffer, rhs.buffer); - len = rhs.len; - rhs.len = 0; - return; - } else { - free(buffer); - } - } - buffer = rhs.buffer; - capacity = rhs.capacity; - len = rhs.len; - rhs.buffer = NULL; - rhs.capacity = 0; - rhs.len = 0; -} -#endif - -String & String::operator = (const String &rhs) -{ - if (this == &rhs) return *this; - - if (rhs.buffer) copy(rhs.buffer, rhs.len); - else invalidate(); - - return *this; -} - -#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__) -String & String::operator = (String &&rval) -{ - if (this != &rval) move(rval); - return *this; -} - -String & String::operator = (StringSumHelper &&rval) -{ - if (this != &rval) move(rval); - return *this; -} -#endif - -String & String::operator = (const char *cstr) -{ - if (cstr) copy(cstr, strlen(cstr)); - else invalidate(); - - return *this; -} - -String & String::operator = (const __FlashStringHelper *pstr) -{ - // if (pstr) copy(pstr, strlen_P((PGM_P)pstr)); - // else invalidate(); - - return *this; -} - -/*********************************************/ -/* concat */ -/*********************************************/ - -unsigned char String::concat(const String &s) -{ - return concat(s.buffer, s.len); -} - -unsigned char String::concat(const char *cstr, unsigned int length) -{ - unsigned int newlen = len + length; - if (!cstr) return 0; - if (length == 0) return 1; - if (!reserve(newlen)) return 0; - strcpy(buffer + len, cstr); - len = newlen; - return 1; -} - -unsigned char String::concat(const char *cstr) -{ - if (!cstr) return 0; - return concat(cstr, strlen(cstr)); -} - -unsigned char String::concat(char c) -{ - char buf[2]; - buf[0] = c; - buf[1] = 0; - return concat(buf, 1); -} - -unsigned char String::concat(unsigned char num) -{ - char buf[1 + 3 * sizeof(unsigned char)]; - itoa(num, buf, 10); - return concat(buf, strlen(buf)); -} - -unsigned char String::concat(int num) -{ - char buf[2 + 3 * sizeof(int)]; - itoa(num, buf, 10); - return concat(buf, strlen(buf)); -} - -unsigned char String::concat(unsigned int num) -{ - char buf[1 + 3 * sizeof(unsigned int)]; - itoa(num, buf, 10); - return concat(buf, strlen(buf)); -} - -unsigned char String::concat(long num) -{ - char buf[2 + 3 * sizeof(long)]; - ltoa(num, buf, 10); - return concat(buf, strlen(buf)); -} - -unsigned char String::concat(unsigned long num) -{ - char buf[1 + 3 * sizeof(unsigned long)]; - ultoa(num, buf, 10); - return concat(buf, strlen(buf)); -} - -unsigned char String::concat(float num) -{ - char buf[20]; - char* string = dtostrf(num, 4, 2, buf); - return concat(string, strlen(string)); -} - -unsigned char String::concat(double num) -{ - char buf[20]; - char* string = dtostrf(num, 4, 2, buf); - return concat(string, strlen(string)); -} - -unsigned char String::concat(const __FlashStringHelper * str) -{ - if (!str) return 0; - int length = strlen_P((const char *) str); - if (length == 0) return 1; - unsigned int newlen = len + length; - if (!reserve(newlen)) return 0; - strcpy_P(buffer + len, (const char *) str); - len = newlen; - return 1; -} - -/*********************************************/ -/* Concatenate */ -/*********************************************/ - -StringSumHelper & operator + (const StringSumHelper &lhs, const String &rhs) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(rhs.buffer, rhs.len)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, const char *cstr) -{ - StringSumHelper &a = const_cast(lhs); - if (!cstr || !a.concat(cstr, strlen(cstr))) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, char c) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(c)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, unsigned char num) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(num)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, int num) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(num)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, unsigned int num) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(num)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, long num) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(num)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, unsigned long num) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(num)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, float num) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(num)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, double num) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(num)) a.invalidate(); - return a; -} - -StringSumHelper & operator + (const StringSumHelper &lhs, const __FlashStringHelper *rhs) -{ - StringSumHelper &a = const_cast(lhs); - if (!a.concat(rhs)) a.invalidate(); - return a; -} - -/*********************************************/ -/* Comparison */ -/*********************************************/ - -int String::compareTo(const String &s) const -{ - if (!buffer || !s.buffer) { - if (s.buffer && s.len > 0) return 0 - *(unsigned char *)s.buffer; - if (buffer && len > 0) return *(unsigned char *)buffer; - return 0; - } - return strcmp(buffer, s.buffer); -} - -unsigned char String::isEmpty() const { - return (len == 0); -} - -unsigned char String::equals(const String &s2) const -{ - return (len == s2.len && compareTo(s2) == 0); -} - -unsigned char String::equals(const char *cstr) const -{ - if (len == 0) return (cstr == NULL || *cstr == 0); - if (cstr == NULL) return buffer[0] == 0; - return strcmp(buffer, cstr) == 0; -} - -unsigned char String::operator<(const String &rhs) const -{ - return compareTo(rhs) < 0; -} - -unsigned char String::operator>(const String &rhs) const -{ - return compareTo(rhs) > 0; -} - -unsigned char String::operator<=(const String &rhs) const -{ - return compareTo(rhs) <= 0; -} - -unsigned char String::operator>=(const String &rhs) const -{ - return compareTo(rhs) >= 0; -} - -unsigned char String::equalsIgnoreCase( const String &s2 ) const -{ - if (this == &s2) return 1; - if (len != s2.len) return 0; - if (len == 0) return 1; - const char *p1 = buffer; - const char *p2 = s2.buffer; - while (*p1) { - if (tolower(*p1++) != tolower(*p2++)) return 0; - } - return 1; -} - -unsigned char String::startsWith( const String &s2 ) const -{ - if (len < s2.len) return 0; - return startsWith(s2, 0); -} - -unsigned char String::startsWith( const String &s2, unsigned int offset ) const -{ - if (offset > len - s2.len || !buffer || !s2.buffer) return 0; - return strncmp( &buffer[offset], s2.buffer, s2.len ) == 0; -} - -unsigned char String::endsWith( const String &s2 ) const -{ - if ( len < s2.len || !buffer || !s2.buffer) return 0; - return strcmp(&buffer[len - s2.len], s2.buffer) == 0; -} - -/*********************************************/ -/* Character Access */ -/*********************************************/ - -char String::charAt(unsigned int loc) const -{ - return operator[](loc); -} - -void String::setCharAt(unsigned int loc, char c) -{ - if (loc < len) buffer[loc] = c; -} - -char & String::operator[](unsigned int index) -{ - static char dummy_writable_char; - if (index >= len || !buffer) { - dummy_writable_char = 0; - return dummy_writable_char; - } - return buffer[index]; -} - -char String::operator[]( unsigned int index ) const -{ - if (index >= len || !buffer) return 0; - return buffer[index]; -} - -void String::getBytes(unsigned char *buf, unsigned int bufsize, unsigned int index) const -{ - if (!bufsize || !buf) return; - if (index >= len) { - buf[0] = 0; - return; - } - unsigned int n = bufsize - 1; - if (n > len - index) n = len - index; - strncpy((char *)buf, buffer + index, n); - buf[n] = 0; -} - -/*********************************************/ -/* Search */ -/*********************************************/ - -int String::indexOf(char c) const -{ - return indexOf(c, 0); -} - -int String::indexOf( char ch, unsigned int fromIndex ) const -{ - if (fromIndex >= len) return -1; - const char* temp = strchr(buffer + fromIndex, ch); - if (temp == NULL) return -1; - return temp - buffer; -} - -int String::indexOf(const String &s2) const -{ - return indexOf(s2, 0); -} - -int String::indexOf(const String &s2, unsigned int fromIndex) const -{ - if (fromIndex >= len) return -1; - const char *found = strstr(buffer + fromIndex, s2.buffer); - if (found == NULL) return -1; - return found - buffer; -} - -int String::lastIndexOf( char theChar ) const -{ - return lastIndexOf(theChar, len - 1); -} - -int String::lastIndexOf(char ch, unsigned int fromIndex) const -{ - if (fromIndex >= len) return -1; - char tempchar = buffer[fromIndex + 1]; - buffer[fromIndex + 1] = '\0'; - char* temp = strrchr( buffer, ch ); - buffer[fromIndex + 1] = tempchar; - if (temp == NULL) return -1; - return temp - buffer; -} - -int String::lastIndexOf(const String &s2) const -{ - return lastIndexOf(s2, len - s2.len); -} - -int String::lastIndexOf(const String &s2, unsigned int fromIndex) const -{ - if (s2.len == 0 || len == 0 || s2.len > len) return -1; - if (fromIndex >= len) fromIndex = len - 1; - int found = -1; - for (char *p = buffer; p <= buffer + fromIndex; p++) { - p = strstr(p, s2.buffer); - if (!p) break; - if ((unsigned int)(p - buffer) <= fromIndex) found = p - buffer; - } - return found; -} - -String String::substring(unsigned int left, unsigned int right) const -{ - if (left > right) { - unsigned int temp = right; - right = left; - left = temp; - } - String out; - if (left >= len) return out; - if (right > len) right = len; - char temp = buffer[right]; // save the replaced character - buffer[right] = '\0'; - out = buffer + left; // pointer arithmetic - buffer[right] = temp; //restore character - return out; -} - -/*********************************************/ -/* Modification */ -/*********************************************/ - -void String::replace(char find, char replace) -{ - if (!buffer) return; - for (char *p = buffer; *p; p++) { - if (*p == find) *p = replace; - } -} - -void String::replace(const String& find, const String& replace) -{ - if (len == 0 || find.len == 0) return; - int diff = replace.len - find.len; - char *readFrom = buffer; - char *foundAt; - if (diff == 0) { - while ((foundAt = strstr(readFrom, find.buffer)) != NULL) { - memcpy(foundAt, replace.buffer, replace.len); - readFrom = foundAt + replace.len; - } - } else if (diff < 0) { - char *writeTo = buffer; - while ((foundAt = strstr(readFrom, find.buffer)) != NULL) { - unsigned int n = foundAt - readFrom; - memcpy(writeTo, readFrom, n); - writeTo += n; - memcpy(writeTo, replace.buffer, replace.len); - writeTo += replace.len; - readFrom = foundAt + find.len; - len += diff; - } - strcpy(writeTo, readFrom); - } else { - unsigned int size = len; // compute size needed for result - while ((foundAt = strstr(readFrom, find.buffer)) != NULL) { - readFrom = foundAt + find.len; - size += diff; - } - if (size == len) return; - if (size > capacity && !changeBuffer(size)) return; - int index = len - 1; - while (index >= 0 && (index = lastIndexOf(find, index)) >= 0) { - readFrom = buffer + index + find.len; - memmove(readFrom + diff, readFrom, len - (readFrom - buffer)); - len += diff; - buffer[len] = 0; - memcpy(buffer + index, replace.buffer, replace.len); - index--; - } - } -} - -void String::remove(unsigned int index){ - // Pass the biggest integer as the count. The remove method - // below will take care of truncating it at the end of the - // string. - remove(index, (unsigned int)-1); -} - -void String::remove(unsigned int index, unsigned int count){ - if (index >= len) { return; } - if (count <= 0) { return; } - if (count > len - index) { count = len - index; } - char *writeTo = buffer + index; - len = len - count; - strncpy(writeTo, buffer + index + count,len - index); - buffer[len] = 0; -} - -void String::toLowerCase(void) -{ - if (!buffer) return; - for (char *p = buffer; *p; p++) { - *p = tolower(*p); - } -} - -void String::toUpperCase(void) -{ - if (!buffer) return; - for (char *p = buffer; *p; p++) { - *p = toupper(*p); - } -} - -void String::trim(void) -{ - if (!buffer || len == 0) return; - char *begin = buffer; - while (isspace(*begin)) begin++; - char *end = buffer + len - 1; - while (isspace(*end) && end >= begin) end--; - len = end + 1 - begin; - if (begin > buffer) memcpy(buffer, begin, len); - buffer[len] = 0; -} - -/*********************************************/ -/* Parsing / Conversion */ -/*********************************************/ - -long String::toInt(void) const -{ - if (buffer) return atol(buffer); - return 0; -} - -float String::toFloat(void) const -{ - return float(toDouble()); -} - -double String::toDouble(void) const -{ - if (buffer) return atof(buffer); - return 0; -} \ No newline at end of file diff --git a/lib_standalone/WString.h b/lib_standalone/WString.h index 1a7c9d223..c7b481bf2 100644 --- a/lib_standalone/WString.h +++ b/lib_standalone/WString.h @@ -1,236 +1,69 @@ -/* - WString.h - String library for Wiring & Arduino - ...mostly rewritten by Paul Stoffregen... - Copyright (c) 2009-10 Hernando Barragan. All right reserved. - Copyright 2011, Paul Stoffregen, paul@pjrc.com - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +#ifndef WSTRING_H +#define WSTRING_H - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. +#include - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ +// Reproduces Arduino's String class +class String { + public: + String & operator+=(const char * rhs) { + _str += rhs; + return *this; + } -#ifndef String_class_h -#define String_class_h + size_t length() const { + return _str.size(); + } + + String(const char * str = "") + : _str(str) { + } + + const char * c_str() const { + return _str.c_str(); + } + + bool operator==(const char * s) const { + return _str == s; + } + + friend std::ostream & operator<<(std::ostream & lhs, const ::String & rhs) { + lhs << rhs._str; + return lhs; + } + + /// + bool isEmpty() { + return _str.empty(); + } + + // long toInt() const { + // return std::stol(_str); + // } + + bool equals(const char * s) { + return _str == s; + } + + + private: + std::string _str; +}; + +class StringSumHelper; + +inline bool operator==(const std::string & lhs, const ::String & rhs) { + return lhs == rhs.c_str(); +} + + +size_t strlcpy(char * __restrict dst, const char * __restrict src, size_t dsize); +size_t strlcat(char * dst, const char * src, size_t siz); #define strlen_P strlen #define strncpy_P strncpy #define strcmp_P strcmp #define strcpy_P strcpy -// #include "pgmspace.h" -// #include "noniso.h" - -#include -#include -#include - -// When compiling programs with this class, the following gcc parameters -// dramatically increase performance and memory (RAM) efficiency, typically -// with little or no increase in code size. -// -felide-constructors -// -std=c++0x - -class __FlashStringHelper; -// #define F(string_literal) (reinterpret_cast(PSTR(string_literal))) - -// An inherited class for holding the result of a concatenation. These -// result objects are assumed to be writable by subsequent concatenations. -class StringSumHelper; - -// The string class -class String -{ - // use a function pointer to allow for "if (s)" without the - // complications of an operator bool(). for more information, see: - // http://www.artima.com/cppsource/safebool.html - typedef void (String::*StringIfHelperType)() const; - void StringIfHelper() const {} - -public: - // constructors - // creates a copy of the initial value. - // if the initial value is null or invalid, or if memory allocation - // fails, the string will be marked as invalid (i.e. "if (s)" will - // be false). - String(const char *cstr = ""); - String(const String &str); - String(const __FlashStringHelper *str); - #if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__) - String(String &&rval); - String(StringSumHelper &&rval); - #endif - explicit String(char c); - explicit String(unsigned char, unsigned char base=10); - explicit String(int, unsigned char base=10); - explicit String(unsigned int, unsigned char base=10); - explicit String(long, unsigned char base=10); - explicit String(unsigned long, unsigned char base=10); - explicit String(float, unsigned char decimalPlaces=2); - explicit String(double, unsigned char decimalPlaces=2); - ~String(void); - - // memory management - // return true on success, false on failure (in which case, the string - // is left unchanged). reserve(0), if successful, will validate an - // invalid string (i.e., "if (s)" will be true afterwards) - unsigned char reserve(unsigned int size); - inline unsigned int length(void) const {return len;} - - // creates a copy of the assigned value. if the value is null or - // invalid, or if the memory allocation fails, the string will be - // marked as invalid ("if (s)" will be false). - String & operator = (const String &rhs); - String & operator = (const char *cstr); - String & operator = (const __FlashStringHelper *str); - #if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__) - String & operator = (String &&rval); - String & operator = (StringSumHelper &&rval); - #endif - - // concatenate (works w/ built-in types) - - // returns true on success, false on failure (in which case, the string - // is left unchanged). if the argument is null or invalid, the - // concatenation is considered unsucessful. - unsigned char concat(const String &str); - unsigned char concat(const char *cstr); - unsigned char concat(char c); - unsigned char concat(unsigned char c); - unsigned char concat(int num); - unsigned char concat(unsigned int num); - unsigned char concat(long num); - unsigned char concat(unsigned long num); - unsigned char concat(float num); - unsigned char concat(double num); - unsigned char concat(const __FlashStringHelper * str); - - // if there's not enough memory for the concatenated value, the string - // will be left unchanged (but this isn't signalled in any way) - String & operator += (const String &rhs) {concat(rhs); return (*this);} - String & operator += (const char *cstr) {concat(cstr); return (*this);} - String & operator += (char c) {concat(c); return (*this);} - String & operator += (unsigned char num) {concat(num); return (*this);} - String & operator += (int num) {concat(num); return (*this);} - String & operator += (unsigned int num) {concat(num); return (*this);} - String & operator += (long num) {concat(num); return (*this);} - String & operator += (unsigned long num) {concat(num); return (*this);} - String & operator += (float num) {concat(num); return (*this);} - String & operator += (double num) {concat(num); return (*this);} - String & operator += (const __FlashStringHelper *str){concat(str); return (*this);} - - friend StringSumHelper & operator + (const StringSumHelper &lhs, const String &rhs); - friend StringSumHelper & operator + (const StringSumHelper &lhs, const char *cstr); - friend StringSumHelper & operator + (const StringSumHelper &lhs, char c); - friend StringSumHelper & operator + (const StringSumHelper &lhs, unsigned char num); - friend StringSumHelper & operator + (const StringSumHelper &lhs, int num); - friend StringSumHelper & operator + (const StringSumHelper &lhs, unsigned int num); - friend StringSumHelper & operator + (const StringSumHelper &lhs, long num); - friend StringSumHelper & operator + (const StringSumHelper &lhs, unsigned long num); - friend StringSumHelper & operator + (const StringSumHelper &lhs, float num); - friend StringSumHelper & operator + (const StringSumHelper &lhs, double num); - friend StringSumHelper & operator + (const StringSumHelper &lhs, const __FlashStringHelper *rhs); - - // comparison (only works w/ Strings and "strings") - operator StringIfHelperType() const { return buffer ? &String::StringIfHelper : 0; } - int compareTo(const String &s) const; - unsigned char equals(const String &s) const; - unsigned char equals(const char *cstr) const; - unsigned char operator == (const String &rhs) const {return equals(rhs);} - unsigned char operator == (const char *cstr) const {return equals(cstr);} - unsigned char operator != (const String &rhs) const {return !equals(rhs);} - unsigned char operator != (const char *cstr) const {return !equals(cstr);} - unsigned char operator < (const String &rhs) const; - unsigned char operator > (const String &rhs) const; - unsigned char operator <= (const String &rhs) const; - unsigned char operator >= (const String &rhs) const; - unsigned char equalsIgnoreCase(const String &s) const; - unsigned char startsWith( const String &prefix) const; - unsigned char startsWith(const String &prefix, unsigned int offset) const; - unsigned char endsWith(const String &suffix) const; - - // character acccess - char charAt(unsigned int index) const; - void setCharAt(unsigned int index, char c); - char operator [] (unsigned int index) const; - char& operator [] (unsigned int index); - void getBytes(unsigned char *buf, unsigned int bufsize, unsigned int index=0) const; - void toCharArray(char *buf, unsigned int bufsize, unsigned int index=0) const - { getBytes((unsigned char *)buf, bufsize, index); } - const char* c_str() const { return buffer; } - char* begin() { return buffer; } - char* end() { return buffer + length(); } - const char* begin() const { return c_str(); } - const char* end() const { return c_str() + length(); } - - // search - int indexOf( char ch ) const; - int indexOf( char ch, unsigned int fromIndex ) const; - int indexOf( const String &str ) const; - int indexOf( const String &str, unsigned int fromIndex ) const; - int lastIndexOf( char ch ) const; - int lastIndexOf( char ch, unsigned int fromIndex ) const; - int lastIndexOf( const String &str ) const; - int lastIndexOf( const String &str, unsigned int fromIndex ) const; - String substring( unsigned int beginIndex ) const { return substring(beginIndex, len); }; - String substring( unsigned int beginIndex, unsigned int endIndex ) const; - - // modification - void replace(char find, char replace); - void replace(const String& find, const String& replace); - void remove(unsigned int index); - void remove(unsigned int index, unsigned int count); - void toLowerCase(void); - void toUpperCase(void); - void trim(void); - - // parsing/conversion - long toInt(void) const; - float toFloat(void) const; - double toDouble(void) const; - - unsigned char isEmpty() const; - -protected: - char *buffer; // the actual char array - unsigned int capacity; // the array length minus one (for the '\0') - unsigned int len; // the String length (not counting the '\0') -protected: - void init(void); - void invalidate(void); - unsigned char changeBuffer(unsigned int maxStrLen); - unsigned char concat(const char *cstr, unsigned int length); - - // copy and move - String & copy(const char *cstr, unsigned int length); - String & copy(const __FlashStringHelper *pstr, unsigned int length); - #if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__) - void move(String &rhs); - #endif -}; - -class StringSumHelper : public String -{ -public: - StringSumHelper(const String &s) : String(s) {} - StringSumHelper(const char *p) : String(p) {} - StringSumHelper(char c) : String(c) {} - StringSumHelper(unsigned char num) : String(num) {} - StringSumHelper(int num) : String(num) {} - StringSumHelper(unsigned int num) : String(num) {} - StringSumHelper(long num) : String(num) {} - StringSumHelper(unsigned long num) : String(num) {} - StringSumHelper(float num) : String(num) {} - StringSumHelper(double num) : String(num) {} -}; - -#endif // String_class_h \ No newline at end of file +#endif diff --git a/src/EMSESPDevicesService.cpp b/src/EMSESPDevicesService.cpp index 8b77d3b94..46d33293f 100644 --- a/src/EMSESPDevicesService.cpp +++ b/src/EMSESPDevicesService.cpp @@ -70,7 +70,9 @@ void EMSESPDevicesService::device_data(AsyncWebServerRequest * request, JsonVari uint8_t id = json["id"]; // get id from selected table row AsyncJsonResponse * response = new AsyncJsonResponse(false, 1024); +#ifndef EMSESP_STANDALONE EMSESP::device_info(id, (JsonObject &)response->getRoot()); +#endif response->setLength(); request->send(response); } else { diff --git a/src/system.cpp b/src/system.cpp index c842f7c22..9c0b1a392 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -197,7 +197,6 @@ void System::syslog_init() { syslog_mark_interval_ = settings.syslog_mark_interval; syslog_host_ = settings.syslog_host; }); - EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { syslog_.hostname(wifiSettings.hostname.c_str()); }); #ifndef EMSESP_STANDALONE syslog_.start(); // syslog service re-start @@ -210,6 +209,7 @@ void System::syslog_init() { syslog_.log_level((uuid::log::Level)syslog_level_); syslog_.mark_interval(syslog_mark_interval_); syslog_.destination(addr); + EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { syslog_.hostname(wifiSettings.hostname.c_str()); }); #endif } @@ -249,7 +249,11 @@ void System::start() { // returns true if OTA is uploading bool System::upload_status() { +#if defined(EMSESP_STANDALONE) + return false; +#elif return upload_status_ || Update.isRunning(); +#endif } void System::upload_status(bool in_progress) { @@ -380,11 +384,13 @@ int8_t System::wifi_quality() { void System::show_users(uuid::console::Shell & shell) { shell.printfln(F("Users:")); +#ifndef EMSESP_STANDALONE EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { for (User user : securitySettings.users) { shell.printfln(F(" username: %s, password: %s, is_admin: %s"), user.username.c_str(), user.password.c_str(), user.admin ? F("yes") : F("no")); } }); +#endif shell.println(); } diff --git a/src/test/test.cpp b/src/test/test.cpp index f8f77ac74..440245cad 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -#if defined(EMSESP_STANADLONE) +#if defined(EMSESP_STANDALONE) #include "test.h" From 56398ccb8511b5a766161a2d004591419c97a794 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 28 Jul 2020 17:20:10 +0200 Subject: [PATCH 43/66] fix standalone --- src/system.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system.cpp b/src/system.cpp index 9c0b1a392..9bac30722 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -251,7 +251,7 @@ void System::start() { bool System::upload_status() { #if defined(EMSESP_STANDALONE) return false; -#elif +#else return upload_status_ || Update.isRunning(); #endif } From 931dc6dd18955778faa206b2b6efd49340a9ee0f Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Thu, 30 Jul 2020 20:43:18 +0200 Subject: [PATCH 44/66] Fix #441 --- src/roomcontrol.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/roomcontrol.cpp b/src/roomcontrol.cpp index 96e801dbb..5db25621c 100644 --- a/src/roomcontrol.cpp +++ b/src/roomcontrol.cpp @@ -65,6 +65,10 @@ void Roomctrl::check(const uint8_t addr, const uint8_t * data) { if (hc_ > 3) { return; } + // no reply if the temperature is not set + if (remotetemp[hc_] == EMS_VALUE_SHORT_NOTSET) { + return; + } // reply to writes with write nack byte if (addr & 0x80) { // it's a write to us nack_write(); // we don't accept writes. From 251fadc2f357b5702c8ed0ca34fd0778ab47e0da Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:16:00 +0200 Subject: [PATCH 45/66] supress warnings --- makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/makefile b/makefile index 9000ae4c9..06928a441 100644 --- a/makefile +++ b/makefile @@ -65,9 +65,9 @@ CPPFLAGS += -g3 CPPFLAGS += -Os CFLAGS += $(CPPFLAGS) -CFLAGS += -Wall -CFLAGS += -Wno-unused -Wno-restrict -CFLAGS += -Wextra +# CFLAGS += -Wall +# CFLAGS += -Wno-unused -Wno-restrict +# CFLAGS += -Wextra CXXFLAGS += $(CFLAGS) -MMD From d655e50e36fab9722bac21451ff1b3230d3db312 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:17:07 +0200 Subject: [PATCH 46/66] turn off watch when starting new console --- src/console.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/console.cpp b/src/console.cpp index a4ede0465..c594e5ba2 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -90,6 +90,7 @@ void EMSESPShell::display_banner() { // turn off watch emsesp::EMSESP::watch_id(WATCH_ID_NONE); + emsesp::EMSESP::watch(EMSESP::WATCH_OFF); } // pre-loads all the console commands into the MAIN context From 2923fae1a6e1e38ccded6786b2cb37c7509b7546 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:17:26 +0200 Subject: [PATCH 47/66] clean up build settings --- factory_settings.ini | 2 -- features.ini | 8 -------- 2 files changed, 10 deletions(-) delete mode 100644 features.ini diff --git a/factory_settings.ini b/factory_settings.ini index 92058a03b..12a99b948 100644 --- a/factory_settings.ini +++ b/factory_settings.ini @@ -36,12 +36,10 @@ build_flags = -D FACTORY_MQTT_PORT=1883 -D FACTORY_MQTT_USERNAME=\"\" -D FACTORY_MQTT_PASSWORD=\"\" - ; if unspecified the devices hardware ID will be used -D FACTORY_MQTT_CLIENT_ID=\"ems-esp\" -D FACTORY_MQTT_KEEP_ALIVE=60 -D FACTORY_MQTT_CLEAN_SESSION=false -D FACTORY_MQTT_MAX_TOPIC_LENGTH=128 ; JWT Secret - ; if unspecified the devices hardware ID will be used -D FACTORY_JWT_SECRET=\"ems-esp-neo\" diff --git a/features.ini b/features.ini deleted file mode 100644 index 00c36df21..000000000 --- a/features.ini +++ /dev/null @@ -1,8 +0,0 @@ -[features] -build_flags = - -D FT_PROJECT=1 - -D FT_SECURITY=1 - -D FT_MQTT=1 - -D FT_NTP=0 - -D FT_OTA=1 - -D FT_UPLOAD_FIRMWARE=1 From c48e5bd5184d86c68db9cdd014333c17d4665188 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:17:57 +0200 Subject: [PATCH 48/66] minor cleanup --- src/devices/boiler.cpp | 3 +-- src/telegram.h | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 7885fe518..2c1aaf626 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -712,8 +712,7 @@ void Boiler::process_UBAMaintenanceData(std::shared_ptr telegram void Boiler::set_warmwater_temp(const uint8_t temperature) { LOG_INFO(F("Setting boiler warm water temperature to %d C"), temperature); write_command(EMS_TYPE_UBAParameterWW, 2, temperature); - // for i9000, see #397 - write_command(EMS_TYPE_UBAFlags, 3, temperature); + write_command(EMS_TYPE_UBAFlags, 3, temperature); // for i9000, see #397 } // flow temp diff --git a/src/telegram.h b/src/telegram.h index fa19c2c5f..14f762a12 100644 --- a/src/telegram.h +++ b/src/telegram.h @@ -135,10 +135,6 @@ class EMSbus { return (ems_mask_ == EMS_MASK_HT3); } - static uint8_t protocol() { - return ems_mask_; - } - static uint8_t ems_mask() { return ems_mask_; } From b20c6d4c01c52344702541eeb735cc5d61e0951e Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:18:09 +0200 Subject: [PATCH 49/66] only add OTA delay for ESP32 --- src/emsesp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emsesp.cpp b/src/emsesp.cpp index e779cbcf4..96e06318c 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -777,7 +777,9 @@ void EMSESP::loop() { // if we're doing an OTA upload, skip MQTT and EMS if (system_.upload_status()) { +#if defined(ESP32) delay(1); // slow down OTA update to avoid getting killed by task watchdog (task_wdt) +#endif return; } From d70fb4171197e6bfd64a050a26d936142b29b3c4 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:18:28 +0200 Subject: [PATCH 50/66] fix watch for CRC Rx errors --- src/telegram.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/telegram.cpp b/src/telegram.cpp index 002374d75..97e2a41ce 100644 --- a/src/telegram.cpp +++ b/src/telegram.cpp @@ -165,17 +165,18 @@ void RxService::add(uint8_t * data, uint8_t length) { // validate the CRC uint8_t crc = calculate_crc(data, length - 1); - - if ((data[length - 1] != crc) && (EMSESP::watch() != EMSESP::Watch::WATCH_OFF)) { - LOG_ERROR(F("Rx: %s %s(CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET); + if (data[length - 1] != crc) { increment_telegram_error_count(); + if (EMSESP::watch() != EMSESP::Watch::WATCH_OFF) { + LOG_ERROR(F("Rx: %s %s(CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET); + } return; } // since it's a valid telegram, work out the ems mask // we check the 1st byte, which assumed is the src ID and see if the MSB (8th bit) is set // this is used to identify if the protocol should be Junkers/HT3 or Buderus - // this only happens once with the first rx telegram is processed + // this only happens once with the first valid rx telegram is processed if (ems_mask() == EMS_MASK_UNSET) { ems_mask(data[0]); } From fc32f0d4b0e3674fdcd00029b6dc0e04891c9af8 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:18:38 +0200 Subject: [PATCH 51/66] merge in build flags --- platformio.ini | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/platformio.ini b/platformio.ini index a0ebe1e25..7879566b2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -7,7 +7,6 @@ default_envs = esp8266 # override any settings with your own local ones in pio_local.ini extra_configs = factory_settings.ini - features.ini pio_local.ini [common] @@ -20,7 +19,12 @@ debug_flags = ; default platformio compile flags are: -fno-rtti -std=c++11 -Os -mlongcalls -mtext-section-literals -falign-functions=4 -ffunction-sections -fdata-sections -fno-exceptions -Wall build_flags = ${factory_settings.build_flags} - ${features.build_flags} + -D FT_PROJECT=1 + -D FT_SECURITY=1 + -D FT_MQTT=1 + -D FT_NTP=0 ; disable NTP for now + -D FT_OTA=1 + -D FT_UPLOAD_FIRMWARE=1 -D ONEWIRE_CRC16=0 -D NO_GLOBAL_ARDUINOOTA -D ARDUINOJSON_ENABLE_STD_STRING=1 @@ -53,8 +57,8 @@ check_flags = ; USB upload ; upload_protocol = esptool -; upload_port = COM6 -; upload_port = /dev/cu.wchusbserial1420 +; upload_port = COM4 +; upload_port = /dev/cu.wchusbserial1410 ; OTA upload upload_protocol = espota From 11fe8e71b4aaea272545f78ef8cd3f519d203b69 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 30 Jul 2020 21:23:57 +0200 Subject: [PATCH 52/66] bump to b10 --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index 96637ec81..8fa5ad086 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "2.0.0b9" +#define EMSESP_APP_VERSION "2.0.0b10" From 34f7796bd8815e37b6545cdf7287f4ff503a079f Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 31 Jul 2020 13:58:20 +0200 Subject: [PATCH 53/66] add NTP service, only for ESP32 --- platformio.ini | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/platformio.ini b/platformio.ini index 7879566b2..a7c1bed39 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,8 +1,8 @@ ; PlatformIO Project Configuration File for EMS-ESP [platformio] -default_envs = esp8266 -; default_envs = esp32 +; default_envs = esp8266 +default_envs = esp32 # override any settings with your own local ones in pio_local.ini extra_configs = @@ -22,7 +22,6 @@ build_flags = -D FT_PROJECT=1 -D FT_SECURITY=1 -D FT_MQTT=1 - -D FT_NTP=0 ; disable NTP for now -D FT_OTA=1 -D FT_UPLOAD_FIRMWARE=1 -D ONEWIRE_CRC16=0 @@ -77,14 +76,15 @@ board_build.f_cpu = 160000000L ; 160MHz ; eagle.flash.4m1m.ld = 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer ; eagle.flash.4m2m.ld = 1019 KB sketch, 2024 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 1028 KB OTA & buffer ; board_build.ldscript = eagle.flash.4m2m.ld -build_flags = ${common.build_flags} ${common.debug_flags} +build_flags = ${common.build_flags} ${common.debug_flags} -DFT_NTP=0 lib_ignore = AsyncTCP [env:esp32] board = esp32dev build_type = release -platform = espressif32 +; platform = espressif32 +platform = https://github.com/platformio/platform-espressif32.git board_build.partitions = min_spiffs.csv ; https://github.com/espressif/arduino-esp32/blob/master/tools/partitions/ lib_deps = ${common.libs_core} -build_flags = ${common.build_flags} ${common.debug_flags} +build_flags = ${common.build_flags} ${common.debug_flags} -DFT_NTP=1 From 1d3a31b9f3fd49ff20a606421372c777b90a31ff Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 31 Jul 2020 13:59:06 +0200 Subject: [PATCH 54/66] show external dallas temp sensors in web UI --- interface/src/project/EMSESP.tsx | 2 +- .../src/project/EMSESPDevicesController.tsx | 2 +- interface/src/project/EMSESPDevicesForm.tsx | 57 ++++++++++++++++++- interface/src/project/EMSESPtypes.ts | 6 ++ src/EMSESPDevicesService.cpp | 25 +++++--- src/emsesp.cpp | 6 +- src/sensors.cpp | 8 +-- src/sensors.h | 2 +- 8 files changed, 87 insertions(+), 21 deletions(-) diff --git a/interface/src/project/EMSESP.tsx b/interface/src/project/EMSESP.tsx index bc2e19971..2d88cd1a9 100644 --- a/interface/src/project/EMSESP.tsx +++ b/interface/src/project/EMSESP.tsx @@ -22,7 +22,7 @@ class EMSESP extends Component { - + diff --git a/interface/src/project/EMSESPDevicesController.tsx b/interface/src/project/EMSESPDevicesController.tsx index dfcb5bf1c..bd035f933 100644 --- a/interface/src/project/EMSESPDevicesController.tsx +++ b/interface/src/project/EMSESPDevicesController.tsx @@ -17,7 +17,7 @@ class EMSESPDevicesController extends Component { render() { return ( - + } diff --git a/interface/src/project/EMSESPDevicesForm.tsx b/interface/src/project/EMSESPDevicesForm.tsx index d90904aba..50673747e 100644 --- a/interface/src/project/EMSESPDevicesForm.tsx +++ b/interface/src/project/EMSESPDevicesForm.tsx @@ -71,14 +71,21 @@ class EMSESPDevicesForm extends Component { + return (this.props.data.sensors.length === 0); + }; + noDeviceData = () => { return (this.state.deviceData?.deviceData.length === 0); }; - createTableItems() { + createDeviceItems() { const { width, data } = this.props; return ( + + Devices: + {!this.noDevices() && ( @@ -132,6 +139,49 @@ class EMSESPDevicesForm extends Component +

+ + Sensors: + + {!this.noSensors() && ( +
+ + + ID + Temperature + + + + {data.sensors.map(sensorData => ( + + + {sensorData.id} + + + {sensorData.temp}°C + + + ))} + +
+ )} + {this.noSensors() && + ( + + + No external temperature sensors detected. + + + ) + } +
+ ); + } + renderScanDevicesDialog() { return (



- {this.createTableItems()} + {this.createDeviceItems()} {this.renderDeviceData()} + {this.createSensorItems()}

diff --git a/interface/src/project/EMSESPtypes.ts b/interface/src/project/EMSESPtypes.ts index 2e510ab4e..43084bf9b 100644 --- a/interface/src/project/EMSESPtypes.ts +++ b/interface/src/project/EMSESPtypes.ts @@ -33,8 +33,14 @@ export interface Device { version: string; } +export interface Sensor { + id: string; + temp: number; +} + export interface EMSESPDevices { devices: Device[]; + sensors: Sensor[]; } export interface DeviceData { diff --git a/src/EMSESPDevicesService.cpp b/src/EMSESPDevicesService.cpp index 46d33293f..15368789a 100644 --- a/src/EMSESPDevicesService.cpp +++ b/src/EMSESPDevicesService.cpp @@ -50,14 +50,23 @@ void EMSESPDevicesService::all_devices(AsyncWebServerRequest * request) { JsonArray devices = root.createNestedArray("devices"); for (const auto & emsdevice : EMSESP::emsdevices) { if (emsdevice) { - JsonObject deviceRoot = devices.createNestedObject(); - deviceRoot["id"] = emsdevice->unique_id(); - deviceRoot["type"] = emsdevice->device_type_name(); - deviceRoot["brand"] = emsdevice->brand_to_string(); - deviceRoot["name"] = emsdevice->name(); - deviceRoot["deviceid"] = emsdevice->device_id(); - deviceRoot["productid"] = emsdevice->product_id(); - deviceRoot["version"] = emsdevice->version(); + JsonObject obj = devices.createNestedObject(); + obj["id"] = emsdevice->unique_id(); + obj["type"] = emsdevice->device_type_name(); + obj["brand"] = emsdevice->brand_to_string(); + obj["name"] = emsdevice->name(); + obj["deviceid"] = emsdevice->device_id(); + obj["productid"] = emsdevice->product_id(); + obj["version"] = emsdevice->version(); + } + } + + JsonArray sensors = root.createNestedArray("sensors"); + if (!EMSESP::sensor_devices().empty()) { + for (const auto & sensor : EMSESP::sensor_devices()) { + JsonObject obj = sensors.createNestedObject(); + obj["id"] = sensor.to_string(); + obj["temp"] = sensor.temperature_c; } } diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 96e06318c..e26fb9e26 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -248,7 +248,7 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) { } } -// show Dallas sensors +// show Dallas temperature sensors void EMSESP::show_sensor_values(uuid::console::Shell & shell) { if (sensor_devices().empty()) { return; @@ -257,7 +257,7 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) { char valuestr[8] = {0}; // for formatting temp shell.printfln(F("External temperature sensors:")); for (const auto & device : sensor_devices()) { - shell.printfln(F(" Sensor ID %s: %s°C"), device.to_string().c_str(), Helpers::render_value(valuestr, device.temperature_c_, 2)); + shell.printfln(F(" ID: %s, Temperature: %s°C"), device.to_string().c_str(), Helpers::render_value(valuestr, device.temperature_c, 2)); } shell.println(); } @@ -778,7 +778,7 @@ void EMSESP::loop() { // if we're doing an OTA upload, skip MQTT and EMS if (system_.upload_status()) { #if defined(ESP32) - delay(1); // slow down OTA update to avoid getting killed by task watchdog (task_wdt) + delay(10); // slow down OTA update to avoid getting killed by task watchdog (task_wdt) #endif return; } diff --git a/src/sensors.cpp b/src/sensors.cpp index c09b71b05..306457f43 100644 --- a/src/sensors.cpp +++ b/src/sensors.cpp @@ -107,7 +107,7 @@ void Sensors::loop() { case TYPE_DS1822: case TYPE_DS1825: found_.emplace_back(addr); - found_.back().temperature_c_ = get_temperature_c(addr); + found_.back().temperature_c = get_temperature_c(addr); /* // comment out for debugging @@ -253,7 +253,7 @@ void Sensors::publish_values() { StaticJsonDocument<100> doc; for (const auto & device : devices_) { char s[5]; - doc["temp"] = Helpers::render_value(s, device.temperature_c_, 2); + doc["temp"] = Helpers::render_value(s, device.temperature_c, 2); char topic[60]; // sensors{1-n} strlcpy(topic, "sensor_", 50); // create topic, e.g. home/ems-esp/sensor_28-EA41-9497-0E03-5F strlcat(topic, device.to_string().c_str(), 60); @@ -279,7 +279,7 @@ void Sensors::publish_values() { for (const auto & device : devices_) { if (mqtt_format_ == MQTT_format::CUSTOM) { char s[5]; - doc[device.to_string()] = Helpers::render_value(s, device.temperature_c_, 2); + doc[device.to_string()] = Helpers::render_value(s, device.temperature_c, 2); } else { char sensorID[10]; // sensor{1-n} strlcpy(sensorID, "sensor", 10); @@ -287,7 +287,7 @@ void Sensors::publish_values() { strlcat(sensorID, Helpers::itoa(s, i++), 10); JsonObject dataSensor = doc.createNestedObject(sensorID); dataSensor["id"] = device.to_string(); - dataSensor["temp"] = Helpers::render_value(s, device.temperature_c_, 2); + dataSensor["temp"] = Helpers::render_value(s, device.temperature_c, 2); } } diff --git a/src/sensors.h b/src/sensors.h index 4601224b0..07a1d0793 100644 --- a/src/sensors.h +++ b/src/sensors.h @@ -46,7 +46,7 @@ class Sensors { uint64_t id() const; std::string to_string() const; - float temperature_c_ = NAN; + float temperature_c = NAN; private: const uint64_t id_; From 4fc6797f4e21b02ccc56df233669fa5e7d9e6ed7 Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 31 Jul 2020 13:59:32 +0200 Subject: [PATCH 55/66] rename collector - #267 --- src/devices/solar.cpp | 10 +++++----- src/devices/solar.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index 2d5c55681..a8b49a9fe 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -77,7 +77,7 @@ void Solar::device_info(JsonArray & root) { } render_value_json(root, "", F("Tank Heated"), tankHeated_, nullptr, EMS_VALUE_BOOL); - render_value_json(root, "", F("Collector"), collectorOnOff_, nullptr, EMS_VALUE_BOOL); + render_value_json(root, "", F("Collector shutdown"), collectorOnOff_, nullptr, EMS_VALUE_BOOL); render_value_json(root, "", F("Energy last hour"), energyLastHour_, F_(wh), 10); render_value_json(root, "", F("Energy today"), energyToday_, F_(wh)); @@ -100,7 +100,7 @@ void Solar::show_values(uuid::console::Shell & shell) { } print_value(shell, 2, F("Tank Heated"), tankHeated_, nullptr, EMS_VALUE_BOOL); - print_value(shell, 2, F("Collector"), collectorOnOff_, nullptr, EMS_VALUE_BOOL); + print_value(shell, 2, F("Collector shutdown"), collectorOnOff_, nullptr, EMS_VALUE_BOOL); print_value(shell, 2, F("Energy last hour"), energyLastHour_, F_(wh), 10); print_value(shell, 2, F("Energy today"), energyToday_, F_(wh)); @@ -229,8 +229,8 @@ void Solar::process_SM100Status(std::shared_ptr telegram) { pumpModulation_ = 15; // set to minimum } - telegram->read_bitvalue(tankHeated_, 3, 1); // issue #422 - telegram->read_bitvalue(collectorOnOff_, 3, 0); + telegram->read_bitvalue(tankHeated_, 3, 1); // issue #422 + telegram->read_bitvalue(collectorOnOff_, 3, 0); // collector shutdown } /* @@ -266,7 +266,7 @@ void Solar::process_ISM1StatusMessage(std::shared_ptr telegram) if (Wh != 0xFFFF) { energyLastHour_ = Wh * 10; // set to *10 } - telegram->read_bitvalue(collectorOnOff_, 9, 0); // collector on/off + telegram->read_bitvalue(collectorOnOff_, 9, 0); // collector shutdown on/off telegram->read_bitvalue(tankHeated_, 9, 2); // tank full } diff --git a/src/devices/solar.h b/src/devices/solar.h index 55b3e398a..1fb9864a1 100644 --- a/src/devices/solar.h +++ b/src/devices/solar.h @@ -58,7 +58,7 @@ class Solar : public EMSdevice { uint32_t energyTotal_ = EMS_VALUE_ULONG_NOTSET; uint32_t pumpWorkMin_ = EMS_VALUE_ULONG_NOTSET; // Total solar pump operating time uint8_t tankHeated_ = EMS_VALUE_BOOL_NOTSET; - uint8_t collectorOnOff_ = EMS_VALUE_BOOL_NOTSET; + uint8_t collectorOnOff_ = EMS_VALUE_BOOL_NOTSET; // Collector shutdown on/off uint8_t availabilityFlag_ = EMS_VALUE_BOOL_NOTSET; uint8_t configFlag_ = EMS_VALUE_BOOL_NOTSET; From 09895bb4610047cef2772463b8ea330dea57598f Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 31 Jul 2020 13:59:40 +0200 Subject: [PATCH 56/66] clean-up code --- lib_standalone/Arduino.cpp | 1 - lib_standalone/AsyncJson.h | 13 +- lib_standalone/AsyncMqttClient.h | 1 - lib_standalone/ESP8266React.h | 2 - lib_standalone/ESPAsyncWebServer.h | 5 +- lib_standalone/FSPersistence.h | 35 ----- lib_standalone/HttpEndpoint.h | 41 ----- lib_standalone/SecurityManager.h | 107 ++++++------- lib_standalone/SecuritySettingsService.h | 118 +++++++------- lib_standalone/StatefulService.h | 190 ++++++++++++----------- 10 files changed, 200 insertions(+), 313 deletions(-) diff --git a/lib_standalone/Arduino.cpp b/lib_standalone/Arduino.cpp index 4e25c2cc6..453a1539e 100644 --- a/lib_standalone/Arduino.cpp +++ b/lib_standalone/Arduino.cpp @@ -37,7 +37,6 @@ static bool __output_pins[256]; static int __output_level[256]; int main(int argc __attribute__((unused)), char * argv[] __attribute__((unused))) { - memset(__output_pins, 0, sizeof(__output_pins)); memset(__output_level, 0, sizeof(__output_level)); diff --git a/lib_standalone/AsyncJson.h b/lib_standalone/AsyncJson.h index e9a36ba76..c957646d5 100644 --- a/lib_standalone/AsyncJson.h +++ b/lib_standalone/AsyncJson.h @@ -3,16 +3,11 @@ #define ASYNC_JSON_H_ #include #include -// #include #define DYNAMIC_JSON_DOCUMENT_SIZE 1024 constexpr const char * JSON_MIMETYPE = "application/json"; -/* - * Json Response - * */ - class ChunkPrint : public Print { private: uint8_t * _destination; @@ -45,9 +40,8 @@ class ChunkPrint : public Print { } }; -class AsyncJsonResponse { +class AsyncJsonResponse { protected: - DynamicJsonDocument _jsonBuffer; JsonVariant _root; @@ -71,7 +65,7 @@ class AsyncJsonResponse { return _isValid; } size_t setLength() { -return 0; + return 0; } size_t getSize() { @@ -79,9 +73,6 @@ return 0; } size_t _fillBuffer(uint8_t * data, size_t len) { - // ChunkPrint dest(data, 0, len); - - // serializeJson(_root, dest); return len; } }; diff --git a/lib_standalone/AsyncMqttClient.h b/lib_standalone/AsyncMqttClient.h index 73a1058ea..65e40b2c8 100644 --- a/lib_standalone/AsyncMqttClient.h +++ b/lib_standalone/AsyncMqttClient.h @@ -24,7 +24,6 @@ struct AsyncMqttClientMessageProperties { bool retain; }; - namespace AsyncMqttClientInternals { typedef std::function OnConnectUserCallback; diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index 718a32b67..6621670c2 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -95,8 +95,6 @@ class EMSESPSettingsService { void begin(); private: - // HttpEndpoint _httpEndpoint; - // FSPersistence _fsPersistence; }; #endif diff --git a/lib_standalone/ESPAsyncWebServer.h b/lib_standalone/ESPAsyncWebServer.h index 35a848485..7b4a188e2 100644 --- a/lib_standalone/ESPAsyncWebServer.h +++ b/lib_standalone/ESPAsyncWebServer.h @@ -68,8 +68,8 @@ typedef std::function ArRequestFilterFunc class AsyncWebHandler { protected: - String _username; - String _password; + String _username; + String _password; public: AsyncWebHandler() @@ -131,7 +131,6 @@ class AsyncWebServer { } void on(const char * uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest){}; - }; diff --git a/lib_standalone/FSPersistence.h b/lib_standalone/FSPersistence.h index 754a90cda..e6c43a69f 100644 --- a/lib_standalone/FSPersistence.h +++ b/lib_standalone/FSPersistence.h @@ -24,46 +24,13 @@ class FSPersistence { } void readFromFS() { - /* - File settingsFile = _fs->open(_filePath, "r"); - - if (settingsFile) { - DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize); - DeserializationError error = deserializeJson(jsonDocument, settingsFile); - if (error == DeserializationError::Ok && jsonDocument.is()) { - JsonObject jsonObject = jsonDocument.as(); - _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater); - settingsFile.close(); - return; - } - settingsFile.close(); - } - */ - // If we reach here we have not been successful in loading the config, - // hard-coded emergency defaults are now applied. - applyDefaults(); } bool writeToFS() { - // create and populate a new json object DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize); JsonObject jsonObject = jsonDocument.to(); _statefulService->read(jsonObject, _stateReader); - - // serialize it to filesystem - /* - File settingsFile = _fs->open(_filePath, "w"); - - // failed to open file, return false - if (!settingsFile) { - return false; - } - - // serialize the data to the file - serializeJson(jsonDocument, settingsFile); - settingsFile.close(); - */ return true; } @@ -90,8 +57,6 @@ class FSPersistence { update_handler_id_t _updateHandlerId; protected: - // We assume the updater supplies sensible defaults if an empty object - // is supplied, this virtual function allows that to be changed. virtual void applyDefaults() { DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize); JsonObject jsonObject = jsonDocument.as(); diff --git a/lib_standalone/HttpEndpoint.h b/lib_standalone/HttpEndpoint.h index c5ce9fefe..ae3d7c89c 100644 --- a/lib_standalone/HttpEndpoint.h +++ b/lib_standalone/HttpEndpoint.h @@ -22,12 +22,6 @@ class HttpGetEndpoint { AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN, size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader), _statefulService(statefulService), _bufferSize(bufferSize) { - /* - server->on(servicePath.c_str(), - HTTP_GET, - securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1), - authenticationPredicate)); - */ } HttpGetEndpoint(JsonStateReader stateReader, @@ -36,9 +30,6 @@ class HttpGetEndpoint { const String& servicePath, size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader), _statefulService(statefulService), _bufferSize(bufferSize) { - /* - server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1)); - */ } protected: @@ -47,12 +38,6 @@ class HttpGetEndpoint { size_t _bufferSize; void fetchSettings(AsyncWebServerRequest* request) { - // AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize); - // JsonObject jsonObject = response->getRoot().to(); - // _statefulService->read(jsonObject, _stateReader); - - // response->setLength(); - // request->send(response); } }; @@ -70,17 +55,7 @@ class HttpPostEndpoint { _stateReader(stateReader), _stateUpdater(stateUpdater), _statefulService(statefulService), - /* - _updateHandler( - servicePath, - securityManager->wrapCallback( - std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), - authenticationPredicate), - bufferSize), - */ _bufferSize(bufferSize) { - //_updateHandler.setMethod(HTTP_POST); - // server->addHandler(&_updateHandler); } HttpPostEndpoint(JsonStateReader stateReader, @@ -92,42 +67,26 @@ class HttpPostEndpoint { _stateReader(stateReader), _stateUpdater(stateUpdater), _statefulService(statefulService), - /* - _updateHandler(servicePath, - std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), - bufferSize), - */ _bufferSize(bufferSize) { - // _updateHandler.setMethod(HTTP_POST); - // server->addHandler(&_updateHandler); } protected: JsonStateReader _stateReader; JsonStateUpdater _stateUpdater; StatefulService* _statefulService; - //AsyncCallbackJsonWebHandler _updateHandler; size_t _bufferSize; void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) { if (!json.is()) { - // request->send(400); return; } JsonObject jsonObject = json.as(); StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater); if (outcome == StateUpdateResult::ERROR) { - // request->send(400); return; } if (outcome == StateUpdateResult::CHANGED) { - // request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); }); } - // AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize); - // jsonObject = response->getRoot().to(); - // _statefulService->read(jsonObject, _stateReader); - // response->setLength(); - // request->send(response); } }; diff --git a/lib_standalone/SecurityManager.h b/lib_standalone/SecurityManager.h index 1f1258e09..327206351 100644 --- a/lib_standalone/SecurityManager.h +++ b/lib_standalone/SecurityManager.h @@ -4,7 +4,6 @@ #include #include #include -// #include #include #include @@ -21,82 +20,64 @@ #define MAX_JWT_SIZE 128 class User { - public: - String username; - String password; - bool admin; + public: + String username; + String password; + bool admin; - public: - User(String username, String password, bool admin) : username(username), password(password), admin(admin) { - } + public: + User(String username, String password, bool admin) + : username(username) + , password(password) + , admin(admin) { + } }; class Authentication { - public: - User* user; - boolean authenticated; + public: + User * user; + boolean authenticated; - public: - Authentication(User& user) : user(new User(user)), authenticated(true) { - } - Authentication() : user(nullptr), authenticated(false) { - } - ~Authentication() { - delete (user); - } + public: + Authentication(User & user) + : user(new User(user)) + , authenticated(true) { + } + Authentication() + : user(nullptr) + , authenticated(false) { + } + ~Authentication() { + delete (user); + } }; -typedef std::function AuthenticationPredicate; +typedef std::function AuthenticationPredicate; class AuthenticationPredicates { - public: - static bool NONE_REQUIRED(Authentication& authentication) { - return true; - }; - static bool IS_AUTHENTICATED(Authentication& authentication) { - return authentication.authenticated; - }; - static bool IS_ADMIN(Authentication& authentication) { - return authentication.authenticated && authentication.user->admin; - }; + public: + static bool NONE_REQUIRED(Authentication & authentication) { + return true; + }; + static bool IS_AUTHENTICATED(Authentication & authentication) { + return authentication.authenticated; + }; + static bool IS_ADMIN(Authentication & authentication) { + return authentication.authenticated && authentication.user->admin; + }; }; class SecurityManager { - public: + public: #if FT_ENABLED(FT_SECURITY) - /* - * Authenticate, returning the user if found - */ - virtual Authentication authenticate(const String& username, const String& password) = 0; - - /* - * Generate a JWT for the user provided - */ - virtual String generateJWT(User* user) = 0; - + virtual Authentication authenticate(const String & username, const String & password) = 0; + virtual String generateJWT(User * user) = 0; #endif - /* - * Check the request header for the Authorization token - */ - virtual Authentication authenticateRequest(AsyncWebServerRequest* request) = 0; - - /** - * Filter a request with the provided predicate, only returning true if the predicate matches. - */ - virtual ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0; - - /** - * Wrap the provided request to provide validation against an AuthenticationPredicate. - */ - virtual ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, - AuthenticationPredicate predicate) = 0; - - /** - * Wrap the provided json request callback to provide validation against an AuthenticationPredicate. - */ - virtual ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest, - AuthenticationPredicate predicate) = 0; + virtual Authentication authenticateRequest(AsyncWebServerRequest * request) = 0; + virtual ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0; + virtual ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate) = 0; + virtual ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest, AuthenticationPredicate predicate) = 0; }; -#endif // end SecurityManager_h +#endif // end SecurityManager_h diff --git a/lib_standalone/SecuritySettingsService.h b/lib_standalone/SecuritySettingsService.h index 801e44a3e..0034d08a4 100644 --- a/lib_standalone/SecuritySettingsService.h +++ b/lib_standalone/SecuritySettingsService.h @@ -30,87 +30,79 @@ #if FT_ENABLED(FT_SECURITY) class SecuritySettings { - public: - String jwtSecret; - std::list users; + public: + String jwtSecret; + std::list users; - static void read(SecuritySettings& settings, JsonObject& root) { - // secret - root["jwt_secret"] = settings.jwtSecret; + static void read(SecuritySettings & settings, JsonObject & root) { + // secret + root["jwt_secret"] = settings.jwtSecret; - // users - JsonArray users = root.createNestedArray("users"); - for (User user : settings.users) { - JsonObject userRoot = users.createNestedObject(); - userRoot["username"] = user.username; - userRoot["password"] = user.password; - userRoot["admin"] = user.admin; + // users + JsonArray users = root.createNestedArray("users"); + for (User user : settings.users) { + JsonObject userRoot = users.createNestedObject(); + userRoot["username"] = user.username; + userRoot["password"] = user.password; + userRoot["admin"] = user.admin; + } } - } - static StateUpdateResult update(JsonObject& root, SecuritySettings& settings) { - // secret - settings.jwtSecret = root["jwt_secret"] | FACTORY_JWT_SECRET; + static StateUpdateResult update(JsonObject & root, SecuritySettings & settings) { + // secret + settings.jwtSecret = root["jwt_secret"] | FACTORY_JWT_SECRET; - // users - settings.users.clear(); - if (root["users"].is()) { - for (JsonVariant user : root["users"].as()) { - settings.users.push_back(User(user["username"], user["password"], user["admin"])); - } - } else { - settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true)); - settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false)); + // users + settings.users.clear(); + if (root["users"].is()) { + for (JsonVariant user : root["users"].as()) { + settings.users.push_back(User(user["username"], user["password"], user["admin"])); + } + } else { + settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true)); + settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false)); + } + return StateUpdateResult::CHANGED; } - return StateUpdateResult::CHANGED; - } }; class SecuritySettingsService : public StatefulService, public SecurityManager { - public: - SecuritySettingsService(AsyncWebServer* server, FS* fs); + public: + SecuritySettingsService(AsyncWebServer * server, FS * fs); - void begin(); + void begin(); - // Functions to implement SecurityManager - Authentication authenticate(const String& username, const String& password); - Authentication authenticateRequest(AsyncWebServerRequest* request); - String generateJWT(User* user); - ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); - ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); - ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate); + // Functions to implement SecurityManager + Authentication authenticate(const String & username, const String & password); + Authentication authenticateRequest(AsyncWebServerRequest * request); + String generateJWT(User * user); + ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); + ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); + ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate); - private: - HttpEndpoint _httpEndpoint; - FSPersistence _fsPersistence; - ArduinoJsonJWT _jwtHandler; + private: + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; + ArduinoJsonJWT _jwtHandler; - void configureJWTHandler(); - - /* - * Lookup the user by JWT - */ - Authentication authenticateJWT(String& jwt); - - /* - * Verify the payload is correct - */ - boolean validatePayload(JsonObject& parsedPayload, User* user); + void configureJWTHandler(); + Authentication authenticateJWT(String & jwt); + boolean validatePayload(JsonObject & parsedPayload, User * user); }; #else class SecuritySettingsService : public SecurityManager { - public: - SecuritySettingsService(AsyncWebServer* server, FS* fs); - ~SecuritySettingsService(); + public: + SecuritySettingsService(AsyncWebServer * server, FS * fs); + ~SecuritySettingsService(); - // minimal set of functions to support framework with security settings disabled - Authentication authenticateRequest(AsyncWebServerRequest* request); - ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); - ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); - ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest, AuthenticationPredicate predicate); + // minimal set of functions to support framework with security settings disabled + Authentication authenticateRequest(AsyncWebServerRequest * request); + ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); + ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); + ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest, AuthenticationPredicate predicate); }; -#endif // end FT_ENABLED(FT_SECURITY) -#endif // end SecuritySettingsService_h +#endif // end FT_ENABLED(FT_SECURITY) +#endif // end SecuritySettingsService_h diff --git a/lib_standalone/StatefulService.h b/lib_standalone/StatefulService.h index c6c38e988..41db7d3d5 100644 --- a/lib_standalone/StatefulService.h +++ b/lib_standalone/StatefulService.h @@ -12,133 +12,137 @@ #endif enum class StateUpdateResult { - CHANGED = 0, // The update changed the state and propagation should take place if required - UNCHANGED, // The state was unchanged, propagation should not take place - ERROR // There was a problem updating the state, propagation should not take place + CHANGED = 0, // The update changed the state and propagation should take place if required + UNCHANGED, // The state was unchanged, propagation should not take place + ERROR // There was a problem updating the state, propagation should not take place }; template -using JsonStateUpdater = std::function; +using JsonStateUpdater = std::function; template -using JsonStateReader = std::function; +using JsonStateReader = std::function; -typedef size_t update_handler_id_t; -typedef std::function StateUpdateCallback; +typedef size_t update_handler_id_t; +typedef std::function StateUpdateCallback; typedef struct StateUpdateHandlerInfo { - static update_handler_id_t currentUpdatedHandlerId; - update_handler_id_t _id; - StateUpdateCallback _cb; - bool _allowRemove; - StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove) : - _id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove){}; + static update_handler_id_t currentUpdatedHandlerId; + update_handler_id_t _id; + StateUpdateCallback _cb; + bool _allowRemove; + StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove) + : _id(++currentUpdatedHandlerId) + , _cb(cb) + , _allowRemove(allowRemove){}; } StateUpdateHandlerInfo_t; template class StatefulService { - public: - template + public: + template #ifdef ESP32 - StatefulService(Args&&... args) : - _state(std::forward(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex()) { - } + StatefulService(Args &&... args) + : _state(std::forward(args)...) + , _accessMutex(xSemaphoreCreateRecursiveMutex()) { + } #else - StatefulService(Args&&... args) : _state(std::forward(args)...) { - } + StatefulService(Args &&... args) + : _state(std::forward(args)...) { + } #endif - update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true) { - if (!cb) { - return 0; + update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true) { + if (!cb) { + return 0; + } + StateUpdateHandlerInfo_t updateHandler(cb, allowRemove); + _updateHandlers.push_back(updateHandler); + return updateHandler._id; } - StateUpdateHandlerInfo_t updateHandler(cb, allowRemove); - _updateHandlers.push_back(updateHandler); - return updateHandler._id; - } - void removeUpdateHandler(update_handler_id_t id) { - for (auto i = _updateHandlers.begin(); i != _updateHandlers.end();) { - if ((*i)._allowRemove && (*i)._id == id) { - i = _updateHandlers.erase(i); - } else { - ++i; - } + void removeUpdateHandler(update_handler_id_t id) { + for (auto i = _updateHandlers.begin(); i != _updateHandlers.end();) { + if ((*i)._allowRemove && (*i)._id == id) { + i = _updateHandlers.erase(i); + } else { + ++i; + } + } } - } - StateUpdateResult update(std::function stateUpdater, const String& originId) { - beginTransaction(); - StateUpdateResult result = stateUpdater(_state); - endTransaction(); - if (result == StateUpdateResult::CHANGED) { - callUpdateHandlers(originId); + StateUpdateResult update(std::function stateUpdater, const String & originId) { + beginTransaction(); + StateUpdateResult result = stateUpdater(_state); + endTransaction(); + if (result == StateUpdateResult::CHANGED) { + callUpdateHandlers(originId); + } + return result; } - return result; - } - StateUpdateResult updateWithoutPropagation(std::function stateUpdater) { - beginTransaction(); - StateUpdateResult result = stateUpdater(_state); - endTransaction(); - return result; - } - - StateUpdateResult update(JsonObject& jsonObject, JsonStateUpdater stateUpdater, const String& originId) { - beginTransaction(); - StateUpdateResult result = stateUpdater(jsonObject, _state); - endTransaction(); - if (result == StateUpdateResult::CHANGED) { - callUpdateHandlers(originId); + StateUpdateResult updateWithoutPropagation(std::function stateUpdater) { + beginTransaction(); + StateUpdateResult result = stateUpdater(_state); + endTransaction(); + return result; } - return result; - } - StateUpdateResult updateWithoutPropagation(JsonObject& jsonObject, JsonStateUpdater stateUpdater) { - beginTransaction(); - StateUpdateResult result = stateUpdater(jsonObject, _state); - endTransaction(); - return result; - } - - void read(std::function stateReader) { - beginTransaction(); - stateReader(_state); - endTransaction(); - } - - void read(JsonObject& jsonObject, JsonStateReader stateReader) { - beginTransaction(); - stateReader(_state, jsonObject); - endTransaction(); - } - - void callUpdateHandlers(const String& originId) { - for (const StateUpdateHandlerInfo_t& updateHandler : _updateHandlers) { - updateHandler._cb(originId); + StateUpdateResult update(JsonObject & jsonObject, JsonStateUpdater stateUpdater, const String & originId) { + beginTransaction(); + StateUpdateResult result = stateUpdater(jsonObject, _state); + endTransaction(); + if (result == StateUpdateResult::CHANGED) { + callUpdateHandlers(originId); + } + return result; } - } - protected: - T _state; + StateUpdateResult updateWithoutPropagation(JsonObject & jsonObject, JsonStateUpdater stateUpdater) { + beginTransaction(); + StateUpdateResult result = stateUpdater(jsonObject, _state); + endTransaction(); + return result; + } - inline void beginTransaction() { + void read(std::function stateReader) { + beginTransaction(); + stateReader(_state); + endTransaction(); + } + + void read(JsonObject & jsonObject, JsonStateReader stateReader) { + beginTransaction(); + stateReader(_state, jsonObject); + endTransaction(); + } + + void callUpdateHandlers(const String & originId) { + for (const StateUpdateHandlerInfo_t & updateHandler : _updateHandlers) { + updateHandler._cb(originId); + } + } + + protected: + T _state; + + inline void beginTransaction() { #ifdef ESP32 - xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); + xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); #endif - } + } - inline void endTransaction() { + inline void endTransaction() { #ifdef ESP32 - xSemaphoreGiveRecursive(_accessMutex); + xSemaphoreGiveRecursive(_accessMutex); #endif - } + } - private: + private: #ifdef ESP32 - SemaphoreHandle_t _accessMutex; + SemaphoreHandle_t _accessMutex; #endif - std::list _updateHandlers; + std::list _updateHandlers; }; -#endif // end StatefulService_h +#endif // end StatefulService_h From 163fbba9177736e87bb2884ddcf66f78ed69a082 Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 31 Jul 2020 16:08:58 +0200 Subject: [PATCH 57/66] 2.0.0b11 - with flow/max/min boiler temps - #59 --- README.md | 3 ++ src/devices/boiler.cpp | 103 +++++++++++++++++++++++++++++++++++++---- src/devices/boiler.h | 14 +++++- src/version.h | 2 +- 4 files changed, 111 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4e2f0e16f..517879735 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ boiler wwonetime wwtemp read + temp + maxpower <%> + minpower <%> thermostat set diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 2c1aaf626..be8026789 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -28,6 +28,9 @@ MAKE_PSTR_WORD(comfort) MAKE_PSTR_WORD(eco) MAKE_PSTR_WORD(intelligent) MAKE_PSTR_WORD(hot) +MAKE_PSTR_WORD(maxpower) +MAKE_PSTR_WORD(minpower) +MAKE_PSTR_WORD(temp) MAKE_PSTR(comfort_mandatory, "") @@ -133,6 +136,33 @@ void Boiler::boiler_cmd(const char * message) { } return; } + + // boiler temp setting + if (strcmp(command, "temp") == 0) { + uint8_t t = doc["data"]; + if (t) { + set_temp(t); + } + return; + } + + // boiler max power setting + if (strcmp(command, "maxpower") == 0) { + uint8_t p = doc["data"]; + if (p) { + set_max_power(p); + } + return; + } + + // boiler min power setting + if (strcmp(command, "minpower") == 0) { + uint8_t p = doc["data"]; + if (p) { + set_min_power(p); + } + return; + } } void Boiler::boiler_cmd_wwactivated(const char * message) { @@ -186,7 +216,7 @@ void Boiler::device_info(JsonArray & root) { // publish values via MQTT void Boiler::publish_values() { - const size_t capacity = JSON_OBJECT_SIZE(47); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/ + const size_t capacity = JSON_OBJECT_SIZE(50); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/ DynamicJsonDocument doc(capacity); char s[10]; // for formatting strings @@ -337,6 +367,16 @@ void Boiler::publish_values() { doc["heatWorkMin"] = heatWorkMin_; } + if (Helpers::hasValue(temp_)) { + doc["heatWorkMin"] = temp_; + } + if (Helpers::hasValue(maxpower_)) { + doc["heatWorkMin"] = maxpower_; + } + if (Helpers::hasValue(setpointpower_)) { + doc["heatWorkMin"] = setpointpower_; + } + if (Helpers::hasValue(serviceCode_)) { doc["serviceCode"] = serviceCodeChar_; doc["serviceCodeNumber"] = serviceCode_; @@ -435,6 +475,11 @@ void Boiler::show_values(uuid::console::Shell & shell) { print_value(shell, 2, F("Boiler circuit pump modulation max power"), pump_mod_max_, F_(percent)); print_value(shell, 2, F("Boiler circuit pump modulation min power"), pump_mod_min_, F_(percent)); + // UBASetPoint - these may differ from the above + print_value(shell, 2, F("Boiler temp"), temp_, F_(degrees)); + print_value(shell, 2, F("Max output power"), maxpower_, F_(percent)); + print_value(shell, 2, F("Set power"), setpointpower_, F_(percent)); + // UBAMonitorSlow if (Helpers::hasValue(extTemp_)) { print_value(shell, 2, F("Outside temperature"), extTemp_, F_(degrees), 10); @@ -670,16 +715,16 @@ void Boiler::process_UBAOutdoorTemp(std::shared_ptr telegram) { telegram->read_value(extTemp_, 0); } +// UBASetPoint 0x1A +void Boiler::process_UBASetPoints(std::shared_ptr telegram) { + telegram->read_value(temp_, 0); // boiler flow temp + telegram->read_value(maxpower_, 1); // max output power in % + telegram->read_value(setpointpower_, 14); // ww pump speed/power? +} + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" -// UBASetPoint 0x1A -// not yet implemented -void Boiler::process_UBASetPoints(std::shared_ptr telegram) { - // uint8_t setpoint = telegram->message_data[0]; // boiler flow temp - // uint8_t ww_power = telegram->message_data[2]; // power in % -} - // 0x35 // not yet implemented void Boiler::process_UBAFlags(std::shared_ptr telegram) { @@ -721,6 +766,24 @@ void Boiler::set_flow_temp(const uint8_t temperature) { write_command(EMS_TYPE_UBASetPoints, 0, temperature); } +// set heating temp +void Boiler::set_temp(const uint8_t temperature) { + LOG_INFO(F("Setting boiler temperature to %d C"), temperature); + write_command(EMS_TYPE_UBAParameters, 1, temperature); +} + +// set min boiler output +void Boiler::set_min_power(const uint8_t power) { + LOG_INFO(F("Setting boiler min power to "), power); + write_command(EMS_TYPE_UBAParameters, 3, power); +} + +// set max temp +void Boiler::set_max_power(const uint8_t power) { + LOG_INFO(F("Setting boiler max power to %d C"), power); + write_command(EMS_TYPE_UBAParameters, 2, power); +} + // 1=hot, 2=eco, 3=intelligent // note some boilers do not have this setting, than it's done by thermostat // on a RC35 it's by EMSESP::send_write_request(0x37, 0x10, 2, &set, 1, 0); (set is 1,2,3) @@ -825,6 +888,30 @@ void Boiler::console_commands(Shell & shell, unsigned int context) { set_flow_temp(Helpers::atoint(arguments.front().c_str())); }); + EMSESPShell::commands->add_command(ShellContext::BOILER, + CommandFlags::ADMIN, + flash_string_vector{F_(temp)}, + flash_string_vector{F_(degrees_mandatory)}, + [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { + set_temp(Helpers::atoint(arguments.front().c_str())); + }); + + EMSESPShell::commands->add_command(ShellContext::BOILER, + CommandFlags::ADMIN, + flash_string_vector{F_(maxpower)}, + flash_string_vector{F_(n_mandatory)}, + [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { + set_max_power(Helpers::atoint(arguments.front().c_str())); + }); + + EMSESPShell::commands->add_command(ShellContext::BOILER, + CommandFlags::ADMIN, + flash_string_vector{F_(minpower)}, + flash_string_vector{F_(n_mandatory)}, + [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { + set_min_power(Helpers::atoint(arguments.front().c_str())); + }); + EMSESPShell::commands->add_command( ShellContext::BOILER, CommandFlags::ADMIN, diff --git a/src/devices/boiler.h b/src/devices/boiler.h index ead6431c7..125ff0f04 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -55,6 +55,7 @@ class Boiler : public EMSdevice { static constexpr uint8_t EMS_TYPE_UBAFunctionTest = 0x1D; static constexpr uint8_t EMS_TYPE_UBAFlags = 0x35; static constexpr uint8_t EMS_TYPE_UBASetPoints = 0x1A; + static constexpr uint8_t EMS_TYPE_UBAParameters = 0x16; static constexpr uint8_t EMS_BOILER_SELFLOWTEMP_HEATING = 20; // was originally 70, changed to 30 for issue #193, then to 20 with issue #344 @@ -121,10 +122,15 @@ class Boiler : public EMSdevice { uint8_t pump_mod_max_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation max. power % uint8_t pump_mod_min_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation min. power + // UBASetPoint + uint8_t temp_ = EMS_VALUE_UINT_NOTSET; // boiler flow temp + uint8_t maxpower_ = EMS_VALUE_UINT_NOTSET; // max output power in % + uint8_t setpointpower_ = EMS_VALUE_UINT_NOTSET; // ww pump speed/power? + + // other internal calculated params uint8_t tap_water_active_ = EMS_VALUE_BOOL_NOTSET; // Hot tap water is on/off uint8_t heating_active_ = EMS_VALUE_BOOL_NOTSET; // Central heating is on/off - - uint8_t pumpMod2_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation from 0xE3 (heatpumps) + uint8_t pumpMod2_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation from 0xE3 (heatpumps) void process_UBAParameterWW(std::shared_ptr telegram); void process_UBAMonitorFast(std::shared_ptr telegram); @@ -155,6 +161,10 @@ class Boiler : public EMSdevice { void set_tapwarmwater_activated(const bool activated); void set_warmwater_onetime(const bool activated); void set_warmwater_circulation(const bool activated); + void set_temp(const uint8_t temperature); + void set_min_power(const uint8_t power); + void set_max_power(const uint8_t power); + // mqtt callbacks void boiler_cmd(const char * message); diff --git a/src/version.h b/src/version.h index 8fa5ad086..0d74e7d44 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "2.0.0b10" +#define EMSESP_APP_VERSION "2.0.0b11" From 7ca593f3746caaa79b0fdd8445ffa2e0c732b4e2 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 1 Aug 2020 12:12:42 +0200 Subject: [PATCH 58/66] enlarge json buffer to hold sensor and boiler values --- src/EMSESPDevicesService.cpp | 4 ++-- src/EMSESPDevicesService.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/EMSESPDevicesService.cpp b/src/EMSESPDevicesService.cpp index 15368789a..8fccbdc32 100644 --- a/src/EMSESPDevicesService.cpp +++ b/src/EMSESPDevicesService.cpp @@ -44,7 +44,7 @@ void EMSESPDevicesService::scan_devices(AsyncWebServerRequest * request) { } void EMSESPDevicesService::all_devices(AsyncWebServerRequest * request) { - AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_STATUS_SIZE); + AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_DEVICE_SIZE); JsonObject root = response->getRoot(); JsonArray devices = root.createNestedArray("devices"); @@ -78,7 +78,7 @@ void EMSESPDevicesService::device_data(AsyncWebServerRequest * request, JsonVari if (json.is()) { uint8_t id = json["id"]; // get id from selected table row - AsyncJsonResponse * response = new AsyncJsonResponse(false, 1024); + AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_DEVICE_SIZE); #ifndef EMSESP_STANDALONE EMSESP::device_info(id, (JsonObject &)response->getRoot()); #endif diff --git a/src/EMSESPDevicesService.h b/src/EMSESPDevicesService.h index 05cc58577..b542e82fc 100644 --- a/src/EMSESPDevicesService.h +++ b/src/EMSESPDevicesService.h @@ -24,7 +24,8 @@ #include #include -#define MAX_EMSESP_STATUS_SIZE 1024 +// #define MAX_EMSESP_STATUS_SIZE 1024 +#define MAX_EMSESP_DEVICE_SIZE 1280 #define EMSESP_DEVICES_SERVICE_PATH "/rest/allDevices" #define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices" From 6a434f0cef2bd165436c00a3f213e84313e2830d Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 1 Aug 2020 12:14:49 +0200 Subject: [PATCH 59/66] round sensor values, no yield for esp32 (dualcore) --- src/sensors.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/sensors.cpp b/src/sensors.cpp index 306457f43..76bf98c39 100644 --- a/src/sensors.cpp +++ b/src/sensors.cpp @@ -23,6 +23,12 @@ MAKE_PSTR(logger_name, "sensors") +#ifdef ESP32 +#define YIELD +#else +#define YIELD yield() +#endif + namespace emsesp { uuid::log::Logger Sensors::logger_{F_(logger_name), uuid::log::Facility::DAEMON}; @@ -63,7 +69,7 @@ void Sensors::loop() { if (time_now - last_activity_ >= READ_INTERVAL_MS) { // LOG_DEBUG(F("Read sensor temperature")); // uncomment for debug if (bus_.reset()) { - yield(); + YIELD; bus_.skip(); bus_.write(CMD_CONVERT_TEMP); @@ -155,17 +161,17 @@ float Sensors::get_temperature_c(const uint8_t addr[]) { LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str()); return NAN; } - yield(); + YIELD; uint8_t scratchpad[SCRATCHPAD_LEN] = {0}; bus_.select(addr); bus_.write(CMD_READ_SCRATCHPAD); bus_.read_bytes(scratchpad, SCRATCHPAD_LEN); - yield(); + YIELD; if (!bus_.reset()) { LOG_ERROR(F("Bus reset failed after reading scratchpad from %s"), Device(addr).to_string().c_str()); return NAN; } - yield(); + YIELD; if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) { LOG_WARNING(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from device %s"), scratchpad[0], @@ -202,7 +208,8 @@ float Sensors::get_temperature_c(const uint8_t addr[]) { break; } - return (float)raw_value / 16; + uint32_t raw = (raw_value *625) / 100; // round to 0.01 + return (float)raw / 100; #else return NAN; #endif From d6c5321a5f73e3c28624c1f877af7be3ff44fd06 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 1 Aug 2020 12:17:31 +0200 Subject: [PATCH 60/66] update uarts --- src/uart/emsuart_esp32.cpp | 67 +++++++------------ src/uart/emsuart_esp32.h | 14 ++-- src/uart/emsuart_esp8266.cpp | 124 ++++++++--------------------------- src/uart/emsuart_esp8266.h | 13 ++-- 4 files changed, 68 insertions(+), 150 deletions(-) diff --git a/src/uart/emsuart_esp32.cpp b/src/uart/emsuart_esp32.cpp index e699bb59b..db9381136 100644 --- a/src/uart/emsuart_esp32.cpp +++ b/src/uart/emsuart_esp32.cpp @@ -34,8 +34,8 @@ static hw_timer_t * timer = NULL; bool drop_next_rx = true; uint8_t tx_mode_ = 0xFF; uint8_t emsTxBuf[EMS_MAXBUFFERSIZE]; -uint8_t emsTxBufIdx; -uint8_t emsTxBufLen; +uint8_t emsTxBufIdx = 0; +uint8_t emsTxBufLen = 0; uint32_t emsTxWait; /* @@ -88,12 +88,15 @@ void IRAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() { portENTER_CRITICAL(&mux); if (emsTxBufIdx < emsTxBufLen) { EMS_UART.fifo.rw_byte = emsTxBuf[emsTxBufIdx]; - timerAlarmWrite(timer, emsTxWait, true); + if (emsTxBufIdx == 1) { + timerAlarmWrite(timer, emsTxWait, true); + } } else if (emsTxBufIdx == emsTxBufLen) { EMS_UART.conf0.txd_inv = 1; - timerAlarmWrite(timer, EMSUART_TX_WAIT_BRK, true); + timerAlarmWrite(timer, EMSUART_TX_BRK_TIMER, true); } else if (emsTxBufIdx == emsTxBufLen + 1) { EMS_UART.conf0.txd_inv = 0; + emsTxBufLen = 0; timerAlarmDisable(timer); } emsTxBufIdx++; @@ -130,7 +133,7 @@ void EMSuart::start(const uint8_t tx_mode) { xTaskCreate(emsuart_recvTask, "emsuart_recvTask", 2048, NULL, configMAX_PRIORITIES - 1, NULL); timer = timerBegin(0, 80, true); // timer prescale to 1 us, countup - timerAttachInterrupt(timer, &emsuart_tx_timer_intr_handler, false); // Timer with level interrupt + timerAttachInterrupt(timer, &emsuart_tx_timer_intr_handler, true); // Timer with edge interrupt restart(); } @@ -140,6 +143,9 @@ void EMSuart::start(const uint8_t tx_mode) { void EMSuart::stop() { EMS_UART.int_ena.val = 0; // disable all intr. EMS_UART.conf0.txd_inv = 0; // stop break + if (emsTxBufLen > 0) { + timerAlarmDisable(timer); + } }; /* @@ -165,44 +171,11 @@ void EMSuart::restart() { } } -/* - * Sends a 11-bit break by inverting the tx-port - */ -void EMSuart::tx_brk() { - EMS_UART.conf0.txd_inv = 1; - delayMicroseconds(EMSUART_TX_WAIT_BRK); - EMS_UART.conf0.txd_inv = 0; -} - /* * Sends a 1-byte poll, ending with a */ void EMSuart::send_poll(const uint8_t data) { - if (tx_mode_ > 5) { // timer controlled modes - emsTxBuf[0] = data; - emsTxBufIdx = 0; - emsTxBufLen = 1; - timerAlarmWrite(timer, emsTxWait, true); // start timer with autoreload - timerAlarmEnable(timer); // first interrupt comes immediately - } else if (tx_mode_ == EMS_TXMODE_DEFAULT) { - volatile uint8_t _usrxc = EMS_UART.status.rxfifo_cnt; - uint16_t timeoutcnt = EMSUART_TX_TIMEOUT; - EMS_UART.fifo.rw_byte = data; - while ((EMS_UART.status.rxfifo_cnt == _usrxc) && (--timeoutcnt > 0)) { - delayMicroseconds(EMSUART_TX_BUSY_WAIT); - } - tx_brk(); - } else if (tx_mode_ == EMS_TXMODE_EMSPLUS) { - EMS_UART.fifo.rw_byte = data; - delayMicroseconds(EMSUART_TX_WAIT_PLUS); - tx_brk(); - } else if (tx_mode_ == EMS_TXMODE_HT3) { - EMS_UART.fifo.rw_byte = data; - delayMicroseconds(EMSUART_TX_WAIT_HT3); - tx_brk(); - } else { - EMS_UART.fifo.rw_byte = data; - } + transmit(&data, 1); } /* @@ -221,7 +194,7 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { } emsTxBufIdx = 0; emsTxBufLen = len; - if (tx_mode_ > 100) { + if (tx_mode_ > 100 && len > 1) { timerAlarmWrite(timer, EMSUART_TX_WAIT_REPLY, true); } else { timerAlarmWrite(timer, emsTxWait, true); // start with autoreload @@ -242,7 +215,9 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { EMS_UART.fifo.rw_byte = buf[i]; delayMicroseconds(EMSUART_TX_WAIT_PLUS); } - tx_brk(); + EMS_UART.conf0.txd_inv = 1; // send + delayMicroseconds(EMSUART_TX_BRK_PLUS); + EMS_UART.conf0.txd_inv = 0; return EMS_TX_STATUS_OK; } @@ -251,11 +226,13 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { EMS_UART.fifo.rw_byte = buf[i]; delayMicroseconds(EMSUART_TX_WAIT_HT3); } - tx_brk(); + EMS_UART.conf0.txd_inv = 1; // send + delayMicroseconds(EMSUART_TX_BRK_HT3); + EMS_UART.conf0.txd_inv = 0; return EMS_TX_STATUS_OK; } - // mode 1 + // mode 1: wait for echo after each byte // flush fifos -- not supported in ESP32 uart #2! // EMS_UART.conf0.rxfifo_rst = 1; // EMS_UART.conf0.txfifo_rst = 1; @@ -267,7 +244,9 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { delayMicroseconds(EMSUART_TX_BUSY_WAIT); // burn CPU cycles... } } - tx_brk(); + EMS_UART.conf0.txd_inv = 1; + delayMicroseconds(EMSUART_TX_BRK_EMS); + EMS_UART.conf0.txd_inv = 0; return EMS_TX_STATUS_OK; } diff --git a/src/uart/emsuart_esp32.h b/src/uart/emsuart_esp32.h index ab2d1fd45..df29566b4 100644 --- a/src/uart/emsuart_esp32.h +++ b/src/uart/emsuart_esp32.h @@ -46,20 +46,25 @@ #define EMS_TXMODE_NEW 4 // for michael's testing // LEGACY -#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud -#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 10) // 10 bt -#define EMSUART_TX_WAIT_REPLY 100000 // delay 100ms after first byte +#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud + +// Timer controlled modes +#define EMSUART_TX_BRK_TIMER (EMSUART_TX_BIT_TIME * 10 + 28) // 10.25 bit times +#define EMSUART_TX_WAIT_REPLY 100000 // delay 100ms after first byte // EMS 1.0 #define EMSUART_TX_BUSY_WAIT (EMSUART_TX_BIT_TIME / 8) // 13 -#define EMSUART_TX_TIMEOUT (32 * EMSUART_TX_BIT_TIME / EMSUART_TX_BUSY_WAIT) // 256 +#define EMSUART_TX_TIMEOUT (20 * EMSUART_TX_BIT_TIME / EMSUART_TX_BUSY_WAIT) +#define EMSUART_TX_BRK_EMS (EMSUART_TX_BIT_TIME * 10) // HT3/Junkers - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) plus 7 bit delay. The -8 is for lag compensation. // since we use a faster processor the lag is negligible #define EMSUART_TX_WAIT_HT3 (EMSUART_TX_BIT_TIME * 17) // 1768 +#define EMSUART_TX_BRK_HT3 (EMSUART_TX_BIT_TIME * 11) // EMS+ - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) and delay of another Bytetime. #define EMSUART_TX_WAIT_PLUS (EMSUART_TX_BIT_TIME * 20) // 2080 +#define EMSUART_TX_BRK_PLUS (EMSUART_TX_BIT_TIME * 11) // customize the GPIO pins for RX and TX here @@ -91,7 +96,6 @@ class EMSuart { static void emsuart_recvTask(void * para); static void IRAM_ATTR emsuart_rx_intr_handler(void * para); static void IRAM_ATTR emsuart_tx_timer_intr_handler(); - static void tx_brk(); }; } // namespace emsesp diff --git a/src/uart/emsuart_esp8266.cpp b/src/uart/emsuart_esp8266.cpp index f168d3f13..3816fed86 100644 --- a/src/uart/emsuart_esp8266.cpp +++ b/src/uart/emsuart_esp8266.cpp @@ -95,10 +95,10 @@ void ICACHE_RAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() { timer1_write(emsTxWait); } else if (emsTxBufIdx == emsTxBufLen) { USC0(EMSUART_UART) |= (1 << UCBRK); // set - timer1_write(EMSUART_TX_WAIT_BRK * 5); + timer1_write(EMSUART_TX_BRK_TIMER); } else { USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset - sending_ = false; + sending_ = false; } } @@ -114,11 +114,6 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_flush_fifos() { * init UART0 driver */ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { - if (tx_mode_ > 100) { - emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode - 90); - } else { - emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode + 10); // bittimes wait to next bytes - } if (tx_mode_ != 0xFF) { // it's a restart no need to configure uart tx_mode_ = tx_mode; restart(); @@ -133,7 +128,6 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { } pEMSRxBuf = paEMSRxBuf[0]; // reset EMS Rx Buffer - ETS_UART_INTR_DISABLE(); ETS_UART_INTR_ATTACH(nullptr, nullptr); // pin settings @@ -153,22 +147,14 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { // UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer (default was 127) // see https://www.espressif.com/sites/default/files/documentation/esp8266-technical_reference_en.pdf // - // change: we set UCFFT to 1 to get an immediate indicator about incoming traffic. - // Otherwise, we're only noticed by UCTOT or RxBRK! // change: don't care, we do not use these interrupts - USC1(EMSUART_UART) = 0; // reset config first + USC1(EMSUART_UART) = 0; // reset config // USC1(EMSUART_UART) = (0x7F << UCFFT) | (0x01 << UCTOT) | (1 << UCTOE); // enable interupts // set interrupts for triggers USIC(EMSUART_UART) = 0xFFFF; // clear all interupts USIE(EMSUART_UART) = 0; // disable all interrupts - // enable rx break, fifo full and timeout. - // but not frame error UIFR (because they are too frequent) or overflow UIOF because our buffer is only max 32 bytes - // change: we don't care about Rx Timeout - it may lead to wrong readouts - // change:we don't care about Fifo full and read only on break-detect - USIE(EMSUART_UART) = (1 << UIBD) | (0 << UIFF) | (0 << UITO); - // set up interrupt callbacks for Rx system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen); @@ -182,10 +168,9 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { drop_next_rx = true; // for sending with large delay in EMS+ mode we use a timer interrupt - timer1_attachInterrupt(emsuart_tx_timer_intr_handler); // Add ISR Function - timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); // 5 MHz timer - ETS_UART_INTR_ENABLE(); - USIE(EMSUART_UART) = (1 << UIBD); + timer1_attachInterrupt(emsuart_tx_timer_intr_handler); + + restart(); } /* @@ -204,67 +189,25 @@ void ICACHE_FLASH_ATTR EMSuart::stop() { */ void ICACHE_FLASH_ATTR EMSuart::restart() { if (USIR(EMSUART_UART) & ((1 << UIBD))) { - USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt + USIC(EMSUART_UART) = (1 << UIBD); // INT clear the detect interrupt drop_next_rx = true; } + if (tx_mode_ > 100) { + emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode_ - 90); + } else { + emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode_ + 10); // bittimes wait to next bytes + } emsTxBufIdx = 0; emsTxBufLen = 0; timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); - USIE(EMSUART_UART) = (1 << UIBD); -} - -/* - * Send a BRK signal - * Which is a 11-bit set of zero's (11 cycles) - */ -void ICACHE_FLASH_ATTR EMSuart::tx_brk() { - // make sure Tx FIFO is empty - while (((USS(EMSUART_UART) >> USTXC) & 0xFF)) { - } - USC0(EMSUART_UART) |= (1 << UCBRK); // set bit - // also for EMS+ there is no need to wait longer, we are finished and can free the bus. - delayMicroseconds(EMSUART_TX_WAIT_BRK); // 1144 - USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear BRK bit + USIE(EMSUART_UART) = (1 << UIBD); // enable interrupt } /* * Sends a 1-byte poll, ending with a - * It's a bit dirty. there is no special wait logic per tx_mode type, fifo flushes or error checking */ -void EMSuart::send_poll(uint8_t data) { - // reset tx-brk, just in case it is accidentally set - USC0(EMSUART_UART) &= ~(1 << UCBRK); - sending_ = true; - - if (tx_mode_ >= 5) { // timer controlled modes - emsTxBuf[0] = data; - emsTxBufIdx = 0; - emsTxBufLen = 1; - timer1_write(emsTxWait); - } else if (tx_mode_ == EMS_TXMODE_NEW) { // hardware controlled modes - USF(EMSUART_UART) = data; - USC0(EMSUART_UART) |= (1 << UCBRK); - } else if (tx_mode_ == EMS_TXMODE_HT3) { - USF(EMSUART_UART) = data; - delayMicroseconds(EMSUART_TX_WAIT_HT3); - tx_brk(); // send - sending_ = false; - } else if (tx_mode_ == EMS_TXMODE_EMSPLUS) { - USF(EMSUART_UART) = data; - delayMicroseconds(EMSUART_TX_WAIT_PLUS); - tx_brk(); // send - sending_ = false; - } else { - // tx_mode 1 - volatile uint8_t _usrxc = (USS(EMSUART_UART) >> USRXC) & 0xFF; - USF(EMSUART_UART) = data; - uint16_t timeoutcnt = EMSUART_TX_TIMEOUT; - while ((((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) && (--timeoutcnt > 0)) { - delayMicroseconds(EMSUART_TX_BUSY_WAIT); // burn CPU cycles... - } - tx_brk(); // send - sending_ = false; - } +void ICACHE_FLASH_ATTR EMSuart::send_poll(uint8_t data) { + transmit(&data, 1); } /* @@ -276,20 +219,18 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { if (len == 0 || len >= EMS_MAXBUFFERSIZE) { return EMS_TX_STATUS_ERR; // nothing or to much to send } - // reset tx-brk, just in case it is accidentally set - USC0(EMSUART_UART) &= ~(1 << UCBRK); - sending_ = true; // timer controlled modes with extra delay if (tx_mode_ >= 5) { + sending_ = true; for (uint8_t i = 0; i < len; i++) { emsTxBuf[i] = buf[i]; } USF(EMSUART_UART) = buf[0]; // send first byte emsTxBufIdx = 0; emsTxBufLen = len; - if (tx_mode_ > 100) { - timer1_write(EMSUART_TX_WAIT_REPLY); + if (tx_mode_ > 100 && len > 1) { + timer1_write(EMSUART_TX_WAIT_REPLY); // large delay after first byte } else { timer1_write(emsTxWait); } @@ -301,7 +242,7 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { for (uint8_t i = 0; i < len; i++) { USF(EMSUART_UART) = buf[i]; } - USC0(EMSUART_UART) |= (1 << UCBRK); // send at the end + USC0(EMSUART_UART) |= (1 << UCBRK); // send at the end, clear by interrupt return EMS_TX_STATUS_OK; } @@ -311,8 +252,9 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { USF(EMSUART_UART) = buf[i]; delayMicroseconds(EMSUART_TX_WAIT_PLUS); // 2070 } - tx_brk(); // send - sending_ = false; + USC0(EMSUART_UART) |= (1 << UCBRK); // set break + delayMicroseconds(EMSUART_TX_BRK_PLUS); + USC0(EMSUART_UART) &= ~(1 << UCBRK); return EMS_TX_STATUS_OK; } @@ -326,8 +268,9 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { // wait until bits are sent on wire delayMicroseconds(EMSUART_TX_WAIT_HT3); } - tx_brk(); // send - sending_ = false; + USC0(EMSUART_UART) |= (1 << UCBRK); // set break bit + delayMicroseconds(EMSUART_TX_BRK_HT3); + USC0(EMSUART_UART) &= ~(1 << UCBRK); return EMS_TX_STATUS_OK; } @@ -356,9 +299,7 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { * */ - // disable rx interrupt // clear Rx status register, resetting the Rx FIFO and flush it - // ETS_UART_INTR_DISABLE(); emsuart_flush_fifos(); // send the bytes along the serial line @@ -371,18 +312,9 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { delayMicroseconds(EMSUART_TX_BUSY_WAIT); // burn CPU cycles... } } - - // we got the whole telegram in the Rx buffer - // on Rx-BRK (bus collision), we simply enable Rx and leave it - // otherwise we send the final Tx-BRK - // worst case, we'll see an additional Rx-BRK... - // neither bus collision nor timeout - send terminating BRK signal - if (!(USIS(EMSUART_UART) & (1 << UIBD))) { - // no bus collision - send terminating BRK signal - tx_brk(); - } - // ETS_UART_INTR_ENABLE(); // open up the FIFO again to start receiving - sending_ = false; + USC0(EMSUART_UART) |= (1 << UCBRK); // snd break + delayMicroseconds(EMSUART_TX_BRK_EMS); + USC0(EMSUART_UART) &= ~(1 << UCBRK); return EMS_TX_STATUS_OK; // send the Tx ok status back } diff --git a/src/uart/emsuart_esp8266.h b/src/uart/emsuart_esp8266.h index 01407e54e..be4d1d9fa 100644 --- a/src/uart/emsuart_esp8266.h +++ b/src/uart/emsuart_esp8266.h @@ -40,22 +40,26 @@ #define EMS_TXMODE_NEW 4 // for michael's testing // LEGACY -#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud -#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 10) -#define EMSUART_TX_WAIT_REPLY 500000 // delay 100ms after first byte +#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud + +// TIMER modes +#define EMSUART_TX_BRK_TIMER (EMSUART_TX_BIT_TIME * 52) // > 10 bittimes for timer modes +#define EMSUART_TX_WAIT_REPLY 500000 // delay 100ms after first byte // EMS 1.0 #define EMSUART_TX_BUSY_WAIT (EMSUART_TX_BIT_TIME / 8) // 13 -// #define EMSUART_TX_TIMEOUT (22 * EMSUART_TX_BIT_TIME / EMSUART_TX_BUSY_WAIT) // 176 // #define EMSUART_TX_TIMEOUT (32 * 8) // 256 for tx_mode 1 - see https://github.com/proddy/EMS-ESP/issues/398#issuecomment-645886277 #define EMSUART_TX_TIMEOUT (220 * 8) // 1760 as in v1.9 (180 ms) +#define EMSUART_TX_BRK_EMS (EMSUART_TX_BIT_TIME * 10) // HT3/Junkers - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) plus 7 bit delay. The -8 is for lag compensation. // since we use a faster processor the lag is negligible #define EMSUART_TX_WAIT_HT3 (EMSUART_TX_BIT_TIME * 17) // 1768 +#define EMSUART_TX_BRK_HT3 (EMSUART_TX_BIT_TIME * 11) // EMS+ - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) and delay of another Bytetime. #define EMSUART_TX_WAIT_PLUS (EMSUART_TX_BIT_TIME * 20) // 2080 +#define EMSUART_TX_BRK_PLUS (EMSUART_TX_BIT_TIME * 11) namespace emsesp { @@ -85,7 +89,6 @@ class EMSuart { static void ICACHE_RAM_ATTR emsuart_rx_intr_handler(void * para); static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events); static void ICACHE_FLASH_ATTR emsuart_flush_fifos(); - static void ICACHE_FLASH_ATTR tx_brk(); static void ICACHE_RAM_ATTR emsuart_tx_timer_intr_handler(); static bool sending_; }; From 2936de3e3ff8eeb6429efdde566c9ae145fe8042 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 1 Aug 2020 12:18:20 +0200 Subject: [PATCH 61/66] add boler values --- src/devices/boiler.cpp | 193 ++++++++++++++++++++++++++++------------- src/devices/boiler.h | 17 +++- 2 files changed, 147 insertions(+), 63 deletions(-) diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index be8026789..64adca5d2 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -30,7 +30,6 @@ MAKE_PSTR_WORD(intelligent) MAKE_PSTR_WORD(hot) MAKE_PSTR_WORD(maxpower) MAKE_PSTR_WORD(minpower) -MAKE_PSTR_WORD(temp) MAKE_PSTR(comfort_mandatory, "") @@ -106,18 +105,50 @@ void Boiler::boiler_cmd(const char * message) { uint8_t t = doc["wwtemp"]; set_warmwater_temp(t); } + if (nullptr != doc["boilhyston"]) { + int8_t t = doc["boilhyston"]; + set_hyst_on(t); + } + if (nullptr != doc["boilhystoff"]) { + uint8_t t = doc["boilhystoff"]; + set_hyst_off(t); + } + if (nullptr != doc["burnperiod"]) { + uint8_t t = doc["burnperiod"]; + set_burn_period(t); + } + if (nullptr != doc["burnminpower"]) { + uint8_t p = doc["burnminpower"]; + set_min_power(p); + } + if (nullptr != doc["burnmaxpower"]) { + uint8_t p = doc["burnmaxpower"]; + set_max_power(p); + } + if (nullptr != doc["pumpdelay"]) { + uint8_t t = doc["pumpdelay"]; + set_pump_delay(t); + } + + if (nullptr != doc["comfort"]) { + const char * data = doc["comfort"]; + if (strcmp((char *)data, "hot") == 0) { + set_warmwater_mode(1); + } else if (strcmp((char *)data, "eco") == 0) { + set_warmwater_mode(2); + } else if (strcmp((char *)data, "intelligent") == 0) { + set_warmwater_mode(3); + } + } const char * command = doc["cmd"]; - if (command == nullptr) { + if (command == nullptr || doc["data"] == nullptr) { return; } // boiler ww comfort setting if (strcmp(command, "comfort") == 0) { const char * data = doc["data"]; - if (data == nullptr) { - return; - } if (strcmp((char *)data, "hot") == 0) { set_warmwater_mode(1); } else if (strcmp((char *)data, "eco") == 0) { @@ -131,36 +162,45 @@ void Boiler::boiler_cmd(const char * message) { // boiler flowtemp setting if (strcmp(command, "flowtemp") == 0) { uint8_t t = doc["data"]; - if (t) { - set_flow_temp(t); - } + set_flow_temp(t); return; } - - // boiler temp setting - if (strcmp(command, "temp") == 0) { + if (strcmp(command, "wwtemp") == 0) { uint8_t t = doc["data"]; - if (t) { - set_temp(t); - } + set_warmwater_temp(t); return; } - // boiler max power setting - if (strcmp(command, "maxpower") == 0) { + if (strcmp(command, "burnmaxpower") == 0) { uint8_t p = doc["data"]; - if (p) { - set_max_power(p); - } + set_max_power(p); return; } // boiler min power setting - if (strcmp(command, "minpower") == 0) { + if (strcmp(command, "burnminpower") == 0) { uint8_t p = doc["data"]; - if (p) { - set_min_power(p); - } + set_min_power(p); + return; + } + if (strcmp(command, "boilhyston") == 0) { + int8_t t = doc["data"]; + set_hyst_on(t); + return; + } + if (strcmp(command, "boilhystoff") == 0) { + uint8_t t = doc["data"]; + set_hyst_off(t); + return; + } + if (strcmp(command, "burnperiod") == 0) { + uint8_t t = doc["data"]; + set_burn_period(t); + return; + } + if (strcmp(command, "pumpdelay") == 0) { + uint8_t t = doc["data"]; + set_pump_delay(t); return; } } @@ -216,7 +256,7 @@ void Boiler::device_info(JsonArray & root) { // publish values via MQTT void Boiler::publish_values() { - const size_t capacity = JSON_OBJECT_SIZE(50); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/ + const size_t capacity = JSON_OBJECT_SIZE(56); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/ DynamicJsonDocument doc(capacity); char s[10]; // for formatting strings @@ -328,7 +368,7 @@ void Boiler::publish_values() { doc["flameCurr"] = (float)(int16_t)flameCurr_ / 10; } if (Helpers::hasValue(heatPmp_, VALUE_BOOL)) { - doc["heatPmp"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL); + doc["heatPump"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL); } if (Helpers::hasValue(fanWork_, VALUE_BOOL)) { doc["fanWork"] = Helpers::render_value(s, fanWork_, EMS_VALUE_BOOL); @@ -340,13 +380,37 @@ void Boiler::publish_values() { doc["wWHeat"] = Helpers::render_value(s, wWHeat_, EMS_VALUE_BOOL); } if (Helpers::hasValue(heating_temp_)) { - doc["heating_temp"] = heating_temp_; + doc["heatingTemp"] = heating_temp_; } if (Helpers::hasValue(pump_mod_max_)) { - doc["pump_mod_max"] = pump_mod_max_; + doc["pumpModMax"] = pump_mod_max_; } if (Helpers::hasValue(pump_mod_min_)) { - doc["pump_mod_min"] = pump_mod_min_; + doc["pumpModMin"] = pump_mod_min_; + } + if (Helpers::hasValue(pumpDelay_)) { + doc["pumpDelay"] = pumpDelay_; + } + if (Helpers::hasValue(burnPeriod_)) { + doc["burnMinPeriod"] = burnPeriod_; + } + if (Helpers::hasValue(burnPowermin_)) { + doc["burnMinPower"] = burnPowermin_; + } + if (Helpers::hasValue(burnPowermax_)) { + doc["burnMaxPower"] = burnPowermax_; + } + if (Helpers::hasValue(boilTemp_on_)) { + doc["boilHystOn"] = boilTemp_on_; + } + if (Helpers::hasValue(boilTemp_off_)) { + doc["boilHystOff"] = boilTemp_off_; + } + if (Helpers::hasValue(setFlowTemp_)) { + doc["setFlowTemp"] = setFlowTemp_; + } + if (Helpers::hasValue(setWWPumpPow_)) { + doc["wWSetPumpPower"] = setWWPumpPow_; } if (Helpers::hasValue(wWStarts_)) { doc["wWStarts"] = wWStarts_; @@ -366,17 +430,6 @@ void Boiler::publish_values() { if (Helpers::hasValue(heatWorkMin_)) { doc["heatWorkMin"] = heatWorkMin_; } - - if (Helpers::hasValue(temp_)) { - doc["heatWorkMin"] = temp_; - } - if (Helpers::hasValue(maxpower_)) { - doc["heatWorkMin"] = maxpower_; - } - if (Helpers::hasValue(setpointpower_)) { - doc["heatWorkMin"] = setpointpower_; - } - if (Helpers::hasValue(serviceCode_)) { doc["serviceCode"] = serviceCodeChar_; doc["serviceCodeNumber"] = serviceCode_; @@ -474,11 +527,17 @@ void Boiler::show_values(uuid::console::Shell & shell) { print_value(shell, 2, F("Heating temperature setting on the boiler"), heating_temp_, F_(degrees)); print_value(shell, 2, F("Boiler circuit pump modulation max power"), pump_mod_max_, F_(percent)); print_value(shell, 2, F("Boiler circuit pump modulation min power"), pump_mod_min_, F_(percent)); + print_value(shell, 2, F("Boiler circuit pump delay time"), pumpDelay_, F("min")); + print_value(shell, 2, F("Boiler temp hysteresis on"), boilTemp_on_, F_(degrees)); + print_value(shell, 2, F("Boiler temp hysteresis off"), boilTemp_off_, F_(degrees)); + print_value(shell, 2, F("Boiler burner min period"), burnPeriod_, F("min")); + print_value(shell, 2, F("Boiler burner min power"), burnPowermin_, F_(percent)); + print_value(shell, 2, F("Boiler burner max power"), burnPowermax_, F_(percent)); // UBASetPoint - these may differ from the above - print_value(shell, 2, F("Boiler temp"), temp_, F_(degrees)); - print_value(shell, 2, F("Max output power"), maxpower_, F_(percent)); - print_value(shell, 2, F("Set power"), setpointpower_, F_(percent)); + print_value(shell, 2, F("Set Flow temperature"), setFlowTemp_, F_(degrees)); + print_value(shell, 2, F("Boiler burner set power"), setBurnPow_, F_(percent)); + print_value(shell, 2, F("Warm Water pump set power"), setWWPumpPow_, F_(percent)); // UBAMonitorSlow if (Helpers::hasValue(extTemp_)) { @@ -591,6 +650,12 @@ void Boiler::process_UBATotalUptime(std::shared_ptr telegram) { */ void Boiler::process_UBAParameters(std::shared_ptr telegram) { telegram->read_value(heating_temp_, 1); + telegram->read_value(burnPowermax_,2); + telegram->read_value(burnPowermin_,3); + telegram->read_value(boilTemp_off_,4); + telegram->read_value(boilTemp_on_,5); + telegram->read_value(burnPeriod_,6); + telegram->read_value(pumpDelay_,8); telegram->read_value(pump_mod_max_, 9); telegram->read_value(pump_mod_min_, 10); } @@ -717,9 +782,9 @@ void Boiler::process_UBAOutdoorTemp(std::shared_ptr telegram) { // UBASetPoint 0x1A void Boiler::process_UBASetPoints(std::shared_ptr telegram) { - telegram->read_value(temp_, 0); // boiler flow temp - telegram->read_value(maxpower_, 1); // max output power in % - telegram->read_value(setpointpower_, 14); // ww pump speed/power? + telegram->read_value(setFlowTemp_, 0); // boiler set temp from thermostat + telegram->read_value(setBurnPow_, 1); // max output power in % + telegram->read_value(setWWPumpPow_, 2); // ww pump speed/power? } #pragma GCC diagnostic push @@ -766,12 +831,6 @@ void Boiler::set_flow_temp(const uint8_t temperature) { write_command(EMS_TYPE_UBASetPoints, 0, temperature); } -// set heating temp -void Boiler::set_temp(const uint8_t temperature) { - LOG_INFO(F("Setting boiler temperature to %d C"), temperature); - write_command(EMS_TYPE_UBAParameters, 1, temperature); -} - // set min boiler output void Boiler::set_min_power(const uint8_t power) { LOG_INFO(F("Setting boiler min power to "), power); @@ -784,6 +843,30 @@ void Boiler::set_max_power(const uint8_t power) { write_command(EMS_TYPE_UBAParameters, 2, power); } +// set oiler on hysteresis +void Boiler::set_hyst_on(const uint8_t temp) { + LOG_INFO(F("Setting boiler hysteresis on to %d C"), temp); + write_command(EMS_TYPE_UBAParameters, 5, temp); +} + +// set boiler off hysteresis +void Boiler::set_hyst_off(const uint8_t temp) { + LOG_INFO(F("Setting boiler hysteresis off to %d C"), temp); + write_command(EMS_TYPE_UBAParameters, 4, temp); +} + +// set min burner period +void Boiler::set_burn_period(const uint8_t t) { + LOG_INFO(F("Setting burner min. period to %d min"), t); + write_command(EMS_TYPE_UBAParameters, 6, t); +} + +// set pump delay +void Boiler::set_pump_delay(const uint8_t t) { + LOG_INFO(F("Setting boiler pump delay to %d min"), t); + write_command(EMS_TYPE_UBAParameters, 8, t); +} + // 1=hot, 2=eco, 3=intelligent // note some boilers do not have this setting, than it's done by thermostat // on a RC35 it's by EMSESP::send_write_request(0x37, 0x10, 2, &set, 1, 0); (set is 1,2,3) @@ -888,14 +971,6 @@ void Boiler::console_commands(Shell & shell, unsigned int context) { set_flow_temp(Helpers::atoint(arguments.front().c_str())); }); - EMSESPShell::commands->add_command(ShellContext::BOILER, - CommandFlags::ADMIN, - flash_string_vector{F_(temp)}, - flash_string_vector{F_(degrees_mandatory)}, - [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { - set_temp(Helpers::atoint(arguments.front().c_str())); - }); - EMSESPShell::commands->add_command(ShellContext::BOILER, CommandFlags::ADMIN, flash_string_vector{F_(maxpower)}, diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 125ff0f04..fc77d7e28 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -121,11 +121,17 @@ class Boiler : public EMSdevice { uint8_t heating_temp_ = EMS_VALUE_UINT_NOTSET; // Heating temperature setting on the boiler uint8_t pump_mod_max_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation max. power % uint8_t pump_mod_min_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation min. power + uint8_t burnPowermin_ = EMS_VALUE_UINT_NOTSET; + uint8_t burnPowermax_ = EMS_VALUE_UINT_NOTSET; + int8_t boilTemp_off_ = EMS_VALUE_INT_NOTSET; + int8_t boilTemp_on_ = EMS_VALUE_UINT_NOTSET; + uint8_t burnPeriod_ = EMS_VALUE_UINT_NOTSET; + uint8_t pumpDelay_ = EMS_VALUE_UINT_NOTSET; // UBASetPoint - uint8_t temp_ = EMS_VALUE_UINT_NOTSET; // boiler flow temp - uint8_t maxpower_ = EMS_VALUE_UINT_NOTSET; // max output power in % - uint8_t setpointpower_ = EMS_VALUE_UINT_NOTSET; // ww pump speed/power? + uint8_t setFlowTemp_ = EMS_VALUE_UINT_NOTSET; // boiler setpoint temp + uint8_t setBurnPow_ = EMS_VALUE_UINT_NOTSET; // max output power in % + uint8_t setWWPumpPow_ = EMS_VALUE_UINT_NOTSET; // ww pump speed/power? // other internal calculated params uint8_t tap_water_active_ = EMS_VALUE_BOOL_NOTSET; // Hot tap water is on/off @@ -161,9 +167,12 @@ class Boiler : public EMSdevice { void set_tapwarmwater_activated(const bool activated); void set_warmwater_onetime(const bool activated); void set_warmwater_circulation(const bool activated); - void set_temp(const uint8_t temperature); void set_min_power(const uint8_t power); void set_max_power(const uint8_t power); + void set_hyst_on(const uint8_t temp); + void set_hyst_off(const uint8_t temp); + void set_burn_period(const uint8_t t); + void set_pump_delay(const uint8_t t); // mqtt callbacks From bbc40e5bfb0d7c04635f40bab82353454c965394 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 1 Aug 2020 12:19:01 +0200 Subject: [PATCH 62/66] add solar values --- src/devices/solar.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index a8b49a9fe..30f8974a7 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -266,11 +266,14 @@ void Solar::process_ISM1StatusMessage(std::shared_ptr telegram) if (Wh != 0xFFFF) { energyLastHour_ = Wh * 10; // set to *10 } + telegram->read_bitvalue(pump_, 8, 0); // Solar pump on (1) or off (0) + telegram->read_value(pumpWorkMin_, 10, 3); // force to 3 bytes telegram->read_bitvalue(collectorOnOff_, 9, 0); // collector shutdown on/off telegram->read_bitvalue(tankHeated_, 9, 2); // tank full } /* + * Junkers ISM1 Solar Module - type 0x0101 EMS+ for setting values * e.g. 90 30 FF 06 00 01 50 */ void Solar::process_ISM1Set(std::shared_ptr telegram) { From d2549d025b3677250109308f1f7c83874fde1bee Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 1 Aug 2020 12:20:16 +0200 Subject: [PATCH 63/66] add thermostat values, change output thermostat mqtt-custom to single format --- src/devices/thermostat.cpp | 81 +++++++++++++++++++++++++++----------- src/devices/thermostat.h | 1 + 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index 85916655f..ef61c55d5 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -207,7 +207,17 @@ void Thermostat::device_info(JsonArray & root) { std::string mode_str(15, '\0'); snprintf_P(&mode_str[0], mode_str.capacity() + 1, PSTR("%sMode"), hc_str.c_str()); dataElement["name"] = mode_str; - dataElement["value"] = mode_tostring(hc->get_mode(flags)); + std::string modetype_str(20, '\0'); + if (Helpers::hasValue(hc->summer_mode) && hc->summer_mode) { + snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, PSTR("%s - summer"), mode_tostring(hc->get_mode(flags)).c_str()); + } else if (Helpers::hasValue(hc->holiday_mode) && hc->holiday_mode) { + snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, PSTR("%s - holiday"), mode_tostring(hc->get_mode(flags)).c_str()); + } else if (Helpers::hasValue(hc->mode_type)) { + snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, PSTR("%s - %s"), mode_tostring(hc->get_mode(flags)).c_str(), mode_tostring(hc->get_mode_type(flags)).c_str()); + } else { + snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, mode_tostring(hc->get_mode(flags)).c_str()); + } + dataElement["value"] = modetype_str; } } } @@ -301,7 +311,7 @@ void Thermostat::thermostat_cmd(const char * message) { set_holiday(holiday.c_str(), hc_num); } } - + // commands without heatingcircuit if (nullptr != doc["wwmode"]) { std::string mode = doc["wwmode"]; set_ww_mode(mode); @@ -406,10 +416,10 @@ void Thermostat::thermostat_cmd(const char * message) { // check for commands like {"hc":2,"cmd":"temp","data":21} const char * command = doc["cmd"]; - if (command == nullptr) { + if (command == nullptr || doc["data"] == nullptr) { return; } - + // ok, we have command and data if (strcmp(command, "temp") == 0) { float f = doc["data"]; if (f) { @@ -595,7 +605,8 @@ void Thermostat::publish_values() { } // send this specific data using the thermostat_data topic - if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::HA)) { + // if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::HA)) { + if (mqtt_format_ != MQTT_format::NESTED) { Mqtt::publish("thermostat_data", doc); rootThermostat = doc.to(); // clear object } @@ -609,7 +620,8 @@ void Thermostat::publish_values() { has_data = true; // if the MQTT format is 'nested' or 'ha' then create the parent object hc - if (mqtt_format_ != MQTT_format::SINGLE) { + // if (mqtt_format_ != MQTT_format::SINGLE) { + if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::HA)) { char hc_name[10]; // hc{1-4} strlcpy(hc_name, "hc", 10); char s[3]; @@ -698,7 +710,8 @@ void Thermostat::publish_values() { // if format is single, send immediately and clear object for next hc // the topic will have the hc number appended - if (mqtt_format_ == MQTT_format::SINGLE) { + // if (mqtt_format_ == MQTT_format::SINGLE) { + if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::CUSTOM)) { char topic[30]; char s[3]; strlcpy(topic, "thermostat_data", 30); @@ -717,10 +730,9 @@ void Thermostat::publish_values() { } // if we're using nested json, send all in one go under one topic called thermostat_data + // if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::CUSTOM)) { if (mqtt_format_ == MQTT_format::NESTED) { Mqtt::publish("thermostat_data", doc); - } else if (mqtt_format_ == MQTT_format::CUSTOM) { - Mqtt::publish("thermostat_data", doc); } } @@ -1030,7 +1042,7 @@ void Thermostat::show_values(uuid::console::Shell & shell) { shell.printfln(F(" Display: time")); } else if (ibaMainDisplay_ == 7) { shell.printfln(F(" Display: date")); - } else if (ibaMainDisplay_ == 9) { + } else if (ibaMainDisplay_ == 8) { shell.printfln(F(" Display: smoke temperature")); } } @@ -1209,7 +1221,7 @@ void Thermostat::process_EasyMonitor(std::shared_ptr telegram) { void Thermostat::process_IBASettings(std::shared_ptr telegram) { // 22 - display line on RC35 telegram->read_value(ibaMainDisplay_, - 0); // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 9 smoke temp + 0); // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 8 smoke temp telegram->read_value(ibaLanguage_, 1); // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian telegram->read_value(ibaCalIntTemperature_, 2); // offset int. temperature sensor, by * 0.1 Kelvin telegram->read_value(ibaBuildingType_, 6); // building type: 0 = light, 1 = medium, 2 = heavy @@ -1351,11 +1363,16 @@ void Thermostat::process_RCTime(std::shared_ptr telegram) { if (flags() == EMS_DEVICE_FLAG_EASY) { return; // not supported } - + if (telegram->message_length < 7) { + return; + } + if (telegram->message_data[7] & 0x0C) { // date and time not valid + set_datetime("NTP"); // set from NTP + return; + } if (datetime_.empty()) { datetime_.resize(25, '\0'); } - // render time to HH:MM:SS DD/MM/YYYY // had to create separate buffers because of how printf works char buf1[6]; @@ -1510,18 +1527,38 @@ void Thermostat::set_party(const uint8_t hrs, const uint8_t hc_num) { } } -// set date&time as string hh:mm:ss-dd.mm.yyyy-dw-dst +// set date&time as string hh:mm:ss-dd.mm.yyyy-dw-dst or "NTP" for setting to internet-time // dw - day of week (0..6), dst- summertime (0/1) void Thermostat::set_datetime(const char * dt) { uint8_t data[9]; - data[0] = (dt[16] - '0') * 100 + (dt[17] - '0') * 10 + (dt[18] - '0'); // year - data[1] = (dt[12] - '0') * 10 + (dt[13] - '0'); // month - data[2] = (dt[0] - '0') * 10 + (dt[1] - '0'); // hour - data[3] = (dt[9] - '0') * 10 + (dt[10] - '0'); // day - data[4] = (dt[3] - '0') * 10 + (dt[4] - '0'); // min - data[5] = (dt[6] - '0') * 10 + (dt[7] - '0'); // sec - data[6] = (dt[20] - '0'); // day of week - data[7] = (dt[22] - '0'); // summerime + if (strcmp(dt,"NTP") == 0) { + time_t now = time(nullptr); + tm * tm_ = localtime(&now); + if (tm_->tm_year < 110) { // no NTP time + LOG_WARNING(F("No NTP time. Cannot set RCtime")); + return; + } + data[0] = tm_->tm_year - 100; // Bosch counts from 2000 + data[1] = tm_->tm_mon; + data[2] = tm_->tm_hour; + data[3] = tm_->tm_mday; + data[4] = tm_->tm_min; + data[5] = tm_->tm_sec; + data[6] = (tm_->tm_wday + 6) % 7; // Bosch counts from Mo, time from Su + data[7] = tm_->tm_isdst + 2; // set DST and flag for ext. clock + char time_string[25]; + strftime(time_string, 25, "%FT%T%z", tm_); + LOG_INFO(F("Date and time: %s"), time_string); + } else { + data[0] = (dt[16] - '0') * 100 + (dt[17] - '0') * 10 + (dt[18] - '0'); // year + data[1] = (dt[12] - '0') * 10 + (dt[13] - '0'); // month + data[2] = (dt[0] - '0') * 10 + (dt[1] - '0'); // hour + data[3] = (dt[9] - '0') * 10 + (dt[10] - '0'); // day + data[4] = (dt[3] - '0') * 10 + (dt[4] - '0'); // min + data[5] = (dt[6] - '0') * 10 + (dt[7] - '0'); // sec + data[6] = (dt[20] - '0'); // day of week + data[7] = (dt[22] - '0') + 2; // DST and flag + } if ((flags() & 0x0F) == EMS_DEVICE_FLAG_RC35 || (flags() & 0x0F) == EMS_DEVICE_FLAG_RC30_1) { LOG_INFO(F("Setting date and time")); write_command(6, 0, data, 8, 0); diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index ee019b87b..e0db67f99 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -31,6 +31,7 @@ #include "mqtt.h" #include +#include namespace emsesp { From e735ac03385e5497c7902eecd841a7baf803bd56 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 1 Aug 2020 12:20:55 +0200 Subject: [PATCH 64/66] update readme (add mqtt) --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 517879735..01dc95c21 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ boiler wwonetime wwtemp read - temp maxpower <%> minpower <%> @@ -113,6 +112,61 @@ thermostat ``` ---------- +### **mqtt commands** + +commands can be written as `{"cmd": ,"data": }` or direct. +To set thermostat commands depending on heatingcircuits add `"hc": ` or nest the command. Without `"hc":` the first ative heatingcircuit is set. +These commands are equivalent: +`{"hc":2,"cmd":"daytemp","data":21}` or `{"hc":2,"daytemp":21}`or `{"hc2"{"daytemp":21}}` +In direct commands it's possible to combine commands, i.e. `{"hc1":{"daytemp":21,"nighttemp":17},"hc2":{"daytemp":20}}` +``` +*boiler_cmd* + comfort + flowtemp + wwtemp + boilhyston (negative value) + boilhystoff (positive value) + burnperiod + burnminpower <%> + burnmaxpower <%> + pumpdelay + +*thermostat_cmd* +--- without hc --- + wwmode + calinttemp + minexttemp + building + language (0=de, 1=nl, 2=fr, 3=it) only RC30 + display (0=int temp, 1= int set, 2=ext. temp, 3=burner, 4=ww, 5=mode, 6=time, 7=date, 8=smoke) only RC30 + clockoffset (only RC30) +--- with hc --- + mode + temp + nighttemp + daytemp + nofrosttemp + ecotemp + heattemp + summertemp + designtemp + offsettemp + holidaytemp + remotetemp + control <0 | 1 | 2> + pause + party + holiday + date + +*cmd* + send <"0B XX XX .."> + D0 <0 | 1> + D1 <0 | 1> + D2 <0 | 1> + D3 <0 | 1> + +``` ### **Basic Design Principles** From 91573f55947a6c2ff1f62bdbfb8cd039de178dfe Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sun, 2 Aug 2020 08:54:30 +0200 Subject: [PATCH 65/66] onewire for esp32 improved, retry for sensors --- lib/OneWire/OneWire.cpp | 13 +++++++++++++ lib/OneWire/OneWire.h | 15 +++++++++++++-- src/sensors.cpp | 22 ++++++++++------------ src/sensors.h | 4 +++- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/OneWire/OneWire.cpp b/lib/OneWire/OneWire.cpp index 5828e804d..cd9dc8448 100644 --- a/lib/OneWire/OneWire.cpp +++ b/lib/OneWire/OneWire.cpp @@ -161,7 +161,11 @@ void OneWire::begin(uint8_t pin) { // // Returns 1 if a device asserted a presence pulse, 0 otherwise. // +#ifdef ARDUINO_ARCH_ESP32 +uint8_t IRAM_ATTR OneWire::reset(void) { +#else uint8_t OneWire::reset(void) { +#endif IO_REG_TYPE mask IO_REG_MASK_ATTR = bitmask; volatile IO_REG_TYPE * reg IO_REG_BASE_ATTR = baseReg; uint8_t r; @@ -195,7 +199,11 @@ uint8_t OneWire::reset(void) { // Write a bit. Port and bit is used to cut lookup time and provide // more certain timing. // +#ifdef ARDUINO_ARCH_ESP32 +void IRAM_ATTR OneWire::write_bit(uint8_t v) { +#else void OneWire::write_bit(uint8_t v) { +#endif IO_REG_TYPE mask IO_REG_MASK_ATTR = bitmask; volatile IO_REG_TYPE * reg IO_REG_BASE_ATTR = baseReg; @@ -222,7 +230,11 @@ void OneWire::write_bit(uint8_t v) { // Read a bit. Port and bit is used to cut lookup time and provide // more certain timing. // +#ifdef ARDUINO_ARCH_ESP32 +uint8_t IRAM_ATTR OneWire::read_bit(void) { +#else uint8_t OneWire::read_bit(void) { +#endif IO_REG_TYPE mask IO_REG_MASK_ATTR = bitmask; volatile IO_REG_TYPE * reg IO_REG_BASE_ATTR = baseReg; uint8_t r; @@ -473,6 +485,7 @@ bool OneWire::search(uint8_t * newAddr, bool search_mode /* = true */) { for (int i = 0; i < 8; i++) newAddr[i] = ROM_NO[i]; } + // depower(); // https://github.com/PaulStoffregen/OneWire/pull/80 return search_result; } diff --git a/lib/OneWire/OneWire.h b/lib/OneWire/OneWire.h index 4b1933060..436eecd1f 100644 --- a/lib/OneWire/OneWire.h +++ b/lib/OneWire/OneWire.h @@ -78,7 +78,11 @@ class OneWire { // Perform a 1-Wire reset cycle. Returns 1 if a device responds // with a presence pulse. Returns 0 if there is no device or the // bus is shorted or otherwise held low for more than 250uS +#ifdef ARDUINO_ARCH_ESP32 + uint8_t IRAM_ATTR reset(void); +#else uint8_t reset(void); +#endif // Issue a 1-Wire rom select command, you do the reset first. void select(const uint8_t rom[8]); @@ -101,11 +105,18 @@ class OneWire { // Write a bit. The bus is always left powered at the end, see // note in write() about that. - void write_bit(uint8_t v); + #ifdef ARDUINO_ARCH_ESP32 + void IRAM_ATTR write_bit(uint8_t v); + #else + void write_bit(uint8_t v); +#endif // Read a bit. +#ifdef ARDUINO_ARCH_ESP32 + uint8_t IRAM_ATTR read_bit(void); +#else uint8_t read_bit(void); - +#endif // Stop forcing power onto the bus. You only need to do this if // you used the 'power' flag to write() or used a write_bit() call // and aren't about to do another read or write. You would rather diff --git a/src/sensors.cpp b/src/sensors.cpp index 76bf98c39..26a0a1223 100644 --- a/src/sensors.cpp +++ b/src/sensors.cpp @@ -72,7 +72,6 @@ void Sensors::loop() { YIELD; bus_.skip(); bus_.write(CMD_CONVERT_TEMP); - state_ = State::READING; } else { // no sensors found @@ -86,20 +85,15 @@ void Sensors::loop() { // LOG_DEBUG(F("Scanning for sensors")); // uncomment for debug bus_.reset_search(); found_.clear(); - - state_ = State::SCANNING; - last_activity_ = time_now; + state_ = State::SCANNING; } else if (time_now - last_activity_ > READ_TIMEOUT_MS) { LOG_ERROR(F("Sensor read timeout")); - - state_ = State::IDLE; - last_activity_ = time_now; + state_ = State::IDLE; } } else if (state_ == State::SCANNING) { if (time_now - last_activity_ > SCAN_TIMEOUT_MS) { LOG_ERROR(F("Sensor scan timeout")); - state_ = State::IDLE; - last_activity_ = time_now; + state_ = State::IDLE; } else { uint8_t addr[ADDR_LEN] = {0}; @@ -133,11 +127,15 @@ void Sensors::loop() { } } else { bus_.depower(); - devices_ = std::move(found_); + if ((found_.size() >= devices_.size()) || (retrycnt_ > 5)) { + devices_ = std::move(found_); + retrycnt_ = 0; + } else { + retrycnt_++; + } found_.clear(); // LOG_DEBUG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug - state_ = State::IDLE; - last_activity_ = time_now; + state_ = State::IDLE; } } } diff --git a/src/sensors.h b/src/sensors.h index 07a1d0793..7ec37d6d3 100644 --- a/src/sensors.h +++ b/src/sensors.h @@ -90,7 +90,7 @@ class Sensors { static constexpr uint32_t READ_INTERVAL_MS = 5000; // 5 seconds static constexpr uint32_t CONVERSION_MS = 1000; // 1 seconds static constexpr uint32_t READ_TIMEOUT_MS = 2000; // 2 seconds - static constexpr uint32_t SCAN_TIMEOUT_MS = 30000; // 30 seconds + static constexpr uint32_t SCAN_TIMEOUT_MS = 3000; // 3 seconds static constexpr uint8_t CMD_CONVERT_TEMP = 0x44; static constexpr uint8_t CMD_READ_SCRATCHPAD = 0xBE; @@ -111,6 +111,8 @@ class Sensors { std::vector devices_; uint8_t mqtt_format_; + uint8_t retrycnt_ = 0; + }; } // namespace emsesp From 6df1f3f70867d78114d2eae6cbe9b9788b95ac12 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Mon, 3 Aug 2020 07:00:25 +0200 Subject: [PATCH 66/66] thermostat allow master, snc commands --- src/devices/thermostat.cpp | 48 +++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index ef61c55d5..3468abe06 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -141,8 +141,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i // if we're on auto mode, register this thermostat if it has a device id of 0x10 or 0x17 // or if its the master thermostat we defined // see https://github.com/proddy/EMS-ESP/issues/362#issuecomment-629628161 - if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) && ((device_id == 0x10) || (device_id == 0x17))) - || (master_thermostat == device_id)) { + if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT)) || (master_thermostat == device_id)) { EMSESP::actual_master_thermostat(device_id); LOG_DEBUG(F("Registering new thermostat with device ID 0x%02X (as master)"), device_id); init_mqtt(); @@ -427,8 +426,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - - // thermostat mode changes if (strcmp(command, "mode") == 0) { std::string mode = doc["data"]; if (mode.empty()) { @@ -437,7 +434,6 @@ void Thermostat::thermostat_cmd(const char * message) { set_mode(mode, hc_num); return; } - if (strcmp(command, "nighttemp") == 0) { float f = doc["data"]; if (f) { @@ -445,7 +441,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "daytemp") == 0) { float f = doc["data"]; if (f) { @@ -453,7 +448,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "holidaytemp") == 0) { float f = doc["data"]; if (f) { @@ -461,7 +455,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "ecotemp") == 0) { float f = doc["data"]; if (f) { @@ -469,7 +462,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "heattemp") == 0) { float f = doc["data"]; if (f) { @@ -477,7 +469,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "nofrosttemp") == 0) { float f = doc["data"]; if (f) { @@ -485,7 +476,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "summertemp") == 0) { float f = doc["data"]; if (f) { @@ -493,7 +483,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "designtemp") == 0) { float f = doc["data"]; if (f) { @@ -501,7 +490,6 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } - if (strcmp(command, "offsettemp") == 0) { float f = doc["data"]; if (f) { @@ -509,6 +497,40 @@ void Thermostat::thermostat_cmd(const char * message) { } return; } + if (strcmp(command, "remotetemp") == 0) { + float f = doc["data"]; + if (f > 100 || f < 0) { + Roomctrl::set_remotetemp(hc_num - 1, EMS_VALUE_SHORT_NOTSET); + } else { + Roomctrl::set_remotetemp(hc_num - 1, (int16_t)(f * 10)); + } + return; + } + if (strcmp(command, "control") == 0) { + uint8_t ctrl = doc["data"]; + set_control(ctrl, hc_num); + return; + } + if (strcmp(command, "pause") == 0) { + uint8_t p = doc["data"]; + set_pause(p, hc_num); + return; + } + if (strcmp(command, "party") == 0) { + uint8_t p = doc["data"]; + set_party(p, hc_num); + return; + } + if (strcmp(command, "holiday") == 0) { + std::string holiday = doc["data"]; + set_holiday(holiday.c_str(), hc_num); + return; + } + if (strcmp(command, "date") == 0) { + std::string date = doc["data"]; + set_datetime(date.c_str()); + return; + } } void Thermostat::thermostat_cmd_temp(const char * message) {