From 39ff14cf6e55d72e03d25f5bb503a08cc01e3635 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 11 Mar 2019 21:21:30 +0100 Subject: [PATCH 01/59] fix test for unset values --- src/ems.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ems.cpp b/src/ems.cpp index 0cf9a1124..0ec1d69b3 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -335,7 +335,7 @@ float _toFloat(uint8_t i, uint8_t * data) { if ((data[i] & 0x80) == 0x80) { // check if its an invalid number // 0x8000 is used when sensor is missing - if ((data[i] == 0x80) && (data[i + 1] == 0)) { + if ((data[i] >= 0x80) && (data[i + 1] == 0)) { return (float)EMS_VALUE_FLOAT_NOTSET; // return -1 to indicate that is unknown } // its definitely a negative number From 412ed2fa16cf23a8d5f761025b21386ace86ab04 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 11 Mar 2019 21:22:08 +0100 Subject: [PATCH 02/59] added Moduline 1010 --- src/ems_devices.h | 4 +++- src/my_config.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ems_devices.h b/src/ems_devices.h index 4ac8ab238..bae98b500 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -119,6 +119,7 @@ const _Boiler_Type Boiler_Types[] = { {EMS_MODEL_UBA, 190, 0x09, "BC10 Base Controller"}, {EMS_MODEL_UBA, 114, 0x09, "BC10 Base Controller"}, {EMS_MODEL_UBA, 125, 0x09, "BC25 Base Controller"}, + {EMS_MODEL_UBA, 205, 0x02, "Nefit Moduline Easy Connect"}, {EMS_MODEL_UBA, 68, 0x09, "RFM20 Receiver"}, {EMS_MODEL_UBA, 95, 0x08, "Bosch Condens 2500"}, {EMS_MODEL_UBA, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! @@ -141,6 +142,7 @@ const _Thermostat_Type Thermostat_Types[] = { {EMS_MODEL_BOSCHEASY, 206, 0x02, "Bosch Easy", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_RC310, 158, 0x10, "RC310", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_CW100, 255, 0x18, "Bosch CW100", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES} + {EMS_MODEL_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC10, 165, 0x02, "RC10/Nefit Moduline 1010)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES} }; diff --git a/src/my_config.h b/src/my_config.h index 2c23d611b..b9300c250 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -45,7 +45,7 @@ #define TOPIC_SHOWER_COLDSHOT "shower_coldshot" // used to trigger a coldshot from an MQTT command // default values for shower logic on/off -#define BOILER_SHOWER_TIMER 1 // enable (1) to monitor shower time +#define BOILER_SHOWER_TIMER 0 // enable (1) to monitor shower time #define BOILER_SHOWER_ALERT 0 // enable (1) to send alert of cold water when shower time limit has exceeded #define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water From 007f8c1da19d24bdce150677402d6702c7c6d80c Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 12 Mar 2019 21:54:03 +0100 Subject: [PATCH 03/59] added service code number --- .gitignore | 1 + lib/MyESP/MyESP.h | 2 +- src/ems-esp.ino | 9 +++++---- src/ems.cpp | 6 ++++++ src/ems.h | 30 ++++++++++++++++-------------- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 70ec5cfc7..7d0802d8a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ platformio.ini lib/readme.txt .travis.yml +stackdmp.txt \ No newline at end of file diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 097a767fc..393cae003 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -76,7 +76,7 @@ extern "C" { #define COLOR_BOLD_OFF "\x1B[22m" // fixed by Scott Arlott // SPIFFS -#define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v5/assistant/ +#define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v6/assistant/ // CRASH #define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 4d93fe067..26205d65b 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -327,7 +327,7 @@ void showInfo() { _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); - myDebug(" Current System Service Code: %s", EMS_Boiler.serviceCodeChar); + myDebug(" System Service Code: %s(%d)", EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); // UBAParametersMessage _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); @@ -446,6 +446,7 @@ void publishValues(bool force) { rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; serializeJson(doc, data, sizeof(data)); @@ -512,9 +513,9 @@ void publishValues(bool force) { for (size_t i = 0; i < measureJson(doc) - 1; i++) { crc.update(data[i]); } - uint32_t checksum = crc.finalize(); - if ((previousThermostatPublishCRC != checksum) || force) { - previousThermostatPublishCRC = checksum; + fchecksum = crc.finalize(); + if ((previousThermostatPublishCRC != fchecksum) || force) { + previousThermostatPublishCRC = fchecksum; myDebugLog("Publishing thermostat data via MQTT"); // send values via MQTT diff --git a/src/ems.cpp b/src/ems.cpp index 0ec1d69b3..f14d83ed9 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -203,6 +203,7 @@ void ems_init() { EMS_Boiler.flameCurr = EMS_VALUE_FLOAT_NOTSET; // Flame current in micro amps EMS_Boiler.sysPress = EMS_VALUE_FLOAT_NOTSET; // System pressure strlcpy(EMS_Boiler.serviceCodeChar, "??", sizeof(EMS_Boiler.serviceCodeChar)); + EMS_Boiler.serviceCode = EMS_VALUE_SHORT_NOTSET; // UBAMonitorSlow EMS_Boiler.extTemp = EMS_VALUE_FLOAT_NOTSET; // Outside temperature @@ -925,6 +926,8 @@ void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length) /** * UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long) * received every 10 seconds + * e.g. 08 00 18 00 4B 01 67 02 00 01 01 40 40 01 4B 80 00 01 4A 00 00 0E 30 45 01 09 00 00 00 (CRC=04), #data=25 + * 08 00 18 00 4B 01 56 03 00 01 01 40 40 01 3E 80 00 01 4D 00 00 0E 30 45 01 09 00 00 00 (CRC=EA), #data=25 */ void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.selFlowTemp = data[0]; @@ -948,6 +951,9 @@ void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.serviceCodeChar[0] = char(data[18]); // ascii character 1 EMS_Boiler.serviceCodeChar[1] = char(data[19]); // ascii character 2 + // read error code + EMS_Boiler.serviceCode = (data[20] << 8) + data[21]; + if (data[17] == 0xFF) { // missing value for system pressure EMS_Boiler.sysPress = 0; } else { diff --git a/src/ems.h b/src/ems.h index 5147223b0..2d579fdbf 100644 --- a/src/ems.h +++ b/src/ems.h @@ -27,6 +27,7 @@ #define EMS_VALUE_INT_OFF 0 // boolean false #define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit ints #define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs +#define EMS_VALUE_SHORT_NOTSET 0xFFFF // for 2-byte shorts #define EMS_VALUE_FLOAT_NOTSET -255 // float #define EMS_THERMOSTAT_READ_YES true @@ -158,20 +159,21 @@ typedef struct { // UBAParameterWW uint8_t wWComfort; // Warm water comfort or ECO mode // UBAMonitorFast - uint8_t selFlowTemp; // Selected flow temperature - float curFlowTemp; // Current flow temperature - float retTemp; // Return temperature - uint8_t burnGas; // Gas on/off - uint8_t fanWork; // Fan on/off - uint8_t ignWork; // Ignition on/off - uint8_t heatPmp; // Circulating pump on/off - uint8_t wWHeat; // 3-way valve on WW - uint8_t wWCirc; // Circulation on/off - uint8_t selBurnPow; // Burner max power - uint8_t curBurnPow; // Burner current power - float flameCurr; // Flame current in micro amps - float sysPress; // System pressure - char serviceCodeChar[2]; // 2 character status/service code + uint8_t selFlowTemp; // Selected flow temperature + float curFlowTemp; // Current flow temperature + float retTemp; // Return temperature + uint8_t burnGas; // Gas on/off + uint8_t fanWork; // Fan on/off + uint8_t ignWork; // Ignition on/off + uint8_t heatPmp; // Circulating pump on/off + uint8_t wWHeat; // 3-way valve on WW + uint8_t wWCirc; // Circulation on/off + uint8_t selBurnPow; // Burner max power + uint8_t curBurnPow; // Burner current power + float flameCurr; // Flame current in micro amps + float sysPress; // System pressure + char serviceCodeChar[2]; // 2 character status/service code + uint16_t serviceCode; // error/service code // UBAMonitorSlow float extTemp; // Outside temperature From fb4b23d2cc433cbe8ddc87af8c976553b929bb53 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 12 Mar 2019 22:58:14 +0100 Subject: [PATCH 04/59] fix error with servicecode rendering --- src/ems-esp.ino | 36 ++++++++++++++++++------------------ src/ems.cpp | 9 +++++---- src/ems.h | 2 +- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 26205d65b..3ef90cd38 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -327,7 +327,7 @@ void showInfo() { _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); - myDebug(" System Service Code: %s(%d)", EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); + myDebug(" System Service Code: %s (%d)", EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); // UBAParametersMessage _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); @@ -431,22 +431,22 @@ void publishValues(bool force) { rootBoiler["wWComfort"] = EMS_Boiler.wWComfort ? "Comfort" : "ECO"; rootBoiler["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); snprintf(s, sizeof(s), "%i.%i", EMS_Boiler.wWCurFlow / 10, EMS_Boiler.wWCurFlow % 10); - rootBoiler["wWCurFlow"] = s; - rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); - rootBoiler["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp); - rootBoiler["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp); - rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); - rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); - rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); - rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); - rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); - rootBoiler["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); - rootBoiler["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); - rootBoiler["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); - rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); - rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); - rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; - rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; + rootBoiler["wWCurFlow"] = s; + rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); + rootBoiler["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp); + rootBoiler["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp); + rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); + rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); + rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); + rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); + rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); + rootBoiler["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); + rootBoiler["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); + rootBoiler["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); + rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); + rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); + rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; serializeJson(doc, data, sizeof(data)); @@ -994,7 +994,7 @@ void initEMSESP() { // call PublishValues without forcing, so using CRC to see if we really need to publish void do_publishValues() { // don't publish if we're not connected to the EMS bus - if ((ems_getBusConnected()) && (!myESP.getUseSerial()) && myESP.isMQTTConnected() ) { + if ((ems_getBusConnected()) && (!myESP.getUseSerial()) && myESP.isMQTTConnected()) { publishValues(false); } } diff --git a/src/ems.cpp b/src/ems.cpp index f14d83ed9..8818010f7 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -203,7 +203,7 @@ void ems_init() { EMS_Boiler.flameCurr = EMS_VALUE_FLOAT_NOTSET; // Flame current in micro amps EMS_Boiler.sysPress = EMS_VALUE_FLOAT_NOTSET; // System pressure strlcpy(EMS_Boiler.serviceCodeChar, "??", sizeof(EMS_Boiler.serviceCodeChar)); - EMS_Boiler.serviceCode = EMS_VALUE_SHORT_NOTSET; + EMS_Boiler.serviceCode = EMS_VALUE_SHORT_NOTSET; // UBAMonitorSlow EMS_Boiler.extTemp = EMS_VALUE_FLOAT_NOTSET; // Outside temperature @@ -950,6 +950,7 @@ void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { // read the service code / installation status as appears on the display EMS_Boiler.serviceCodeChar[0] = char(data[18]); // ascii character 1 EMS_Boiler.serviceCodeChar[1] = char(data[19]); // ascii character 2 + EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string // read error code EMS_Boiler.serviceCode = (data[20] << 8) + data[21]; @@ -1514,7 +1515,7 @@ void ems_printAllTypes() { */ void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh) { // if not a valid type of boiler is not accessible then quits - if ( (type == EMS_ID_NONE) || (dest == EMS_ID_NONE) ) { + if ((type == EMS_ID_NONE) || (dest == EMS_ID_NONE)) { return; } @@ -1561,13 +1562,13 @@ void ems_sendRawTelegram(char * telegram) { EMS_Sys_Status.txRetryCount = 0; // reset retry counter // get first value, which should be the src - if ( (p = strtok(telegram, " ,")) ) { // delimiter + if ((p = strtok(telegram, " ,"))) { // delimiter strlcpy(value, p, sizeof(value)); EMS_TxTelegram.data[0] = (uint8_t)strtol(value, 0, 16); } // and interate until end while (p != 0) { - if ( (p = strtok(NULL, " ,")) ) { + if ((p = strtok(NULL, " ,"))) { strlcpy(value, p, sizeof(value)); uint8_t val = (uint8_t)strtol(value, 0, 16); EMS_TxTelegram.data[++count] = val; diff --git a/src/ems.h b/src/ems.h index 2d579fdbf..91e8c3070 100644 --- a/src/ems.h +++ b/src/ems.h @@ -172,7 +172,7 @@ typedef struct { // UBAParameterWW uint8_t curBurnPow; // Burner current power float flameCurr; // Flame current in micro amps float sysPress; // System pressure - char serviceCodeChar[2]; // 2 character status/service code + char serviceCodeChar[3]; // 2 character status/service code uint16_t serviceCode; // error/service code // UBAMonitorSlow From 630c10d8c858394e5595c5070151c5d3a99de011 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 12 Mar 2019 23:03:26 +0100 Subject: [PATCH 05/59] optional compile with CRASH support --- lib/MyESP/MyESP.cpp | 51 ++++++++++++++++++++++++++++++++---------- lib/MyESP/MyESP.h | 2 ++ platformio.ini-example | 2 +- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index d4378fe88..79c116331 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -165,7 +165,7 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { _wifi_callback(); } - jw.enableAPFallback(false); // Disable AP mode after initial connect was succesfull. Thanks @JewelZB + jw.enableAPFallback(false); // Disable AP mode after initial connect was succesfull. Thanks @JewelZB } if (code == MESSAGE_ACCESSPOINT_CREATED) { @@ -291,7 +291,10 @@ void MyESP::_mqtt_setup() { //mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); }); - mqttClient.onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { _mqttOnMessage(topic, payload, len); }); + mqttClient.onMessage( + [this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + _mqttOnMessage(topic, payload, len); + }); } // WiFI setup @@ -407,12 +410,12 @@ void MyESP::_printBuildTime(unsigned long unix) { uint8_t Day, Month; - uint8_t Seconds = unix % 60; /* Get seconds from unix */ - unix /= 60; /* Go to minutes */ - uint8_t Minutes = unix % 60; /* Get minutes */ - unix /= 60; /* Go to hours */ - uint8_t Hours = unix % 24; /* Get hours */ - unix /= 24; /* Go to days */ + uint8_t Seconds = unix % 60; /* Get seconds from unix */ + unix /= 60; /* Go to minutes */ + uint8_t Minutes = unix % 60; /* Get minutes */ + unix /= 60; /* Go to hours */ + uint8_t Hours = unix % 24; /* Get hours */ + unix /= 24; /* Go to days */ uint16_t year = 1970; /* Process year */ while (1) { @@ -468,7 +471,10 @@ void MyESP::_consoleShowHelp() { #else String hostname = WiFi.hostname(); #endif - SerialAndTelnet.printf("* Hostname: %s IP: %s MAC: %s", hostname.c_str(), WiFi.localIP().toString().c_str(), WiFi.macAddress().c_str()); + SerialAndTelnet.printf("* Hostname: %s IP: %s MAC: %s", + hostname.c_str(), + WiFi.localIP().toString().c_str(), + WiFi.macAddress().c_str()); #ifdef ARDUINO_BOARD SerialAndTelnet.printf(" Board: %s", ARDUINO_BOARD); #endif @@ -727,7 +733,7 @@ void MyESP::_telnetCommand(char * commandLine) { crashDump(); } else if (strcmp(cmd, "clear") == 0) { crashClear(); - } else if ((strcmp(cmd, "test") == 0) && (wc == 3) ) { + } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) { char * value = _telnet_readWord(); crashTest(atoi(value)); } @@ -864,7 +870,17 @@ void MyESP::setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_cal } // init MQTT settings -void MyESP::setMQTT(const char * mqtt_host, const char * mqtt_username, const char * mqtt_password, const char * mqtt_base, unsigned long mqtt_keepalive, unsigned char mqtt_qos, bool mqtt_retain, const char * mqtt_will_topic, const char * mqtt_will_online_payload, const char * mqtt_will_offline_payload, mqtt_callback_f callback) { +void MyESP::setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback) { // can be empty if (!mqtt_host || *mqtt_host == 0x00) { _mqtt_host = NULL; @@ -1148,13 +1164,13 @@ int MyESP::getWifiQuality() { return 2 * (dBm + 100); } +#ifdef CRASH /** * Save crash information in EEPROM * This function is called automatically if ESP8266 suffers an exception * It should be kept quick / consise to be able to execute before hardware wdt may kick in */ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end) { - // Note that 'EEPROM.begin' method is reserving a RAM buffer // The buffer size is SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_SPACE_SIZE EEPROM.begin(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EEPROM_SIZE); @@ -1295,6 +1311,17 @@ void MyESP::crashDump() { } myDebug("<< // https://github.com/xoseperez/justwifi #include // modified from https://github.com/yasheena/telnetspy +#ifdef CRASH #include "EEPROM.h" extern "C" { void custom_crash_callback(struct rst_info*, uint32_t, uint32_t); } +#endif #if defined(ARDUINO_ARCH_ESP32) //#include diff --git a/platformio.ini-example b/platformio.ini-example index b381c389d..3687c74f0 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -5,7 +5,7 @@ env_default = d1_mini platform = espressif8266 flash_mode = dout -build_flags_debug = -ggdb3 -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable +build_flags_debug = -ggdb3 -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH ;build_flags_prod = -Os -DBUILD_TIME=$UNIX_TIME build_flags = ${common.build_flags_debug} From 320c71578e6d014c1f80c044a491d3c94a548220 Mon Sep 17 00:00:00 2001 From: Derbyshire Date: Wed, 13 Mar 2019 15:51:45 +0100 Subject: [PATCH 06/59] fix 5V A/C mention --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f9aad9cf8..bc00771bf 100644 --- a/README.md +++ b/README.md @@ -64,14 +64,14 @@ The code and circuit has been tested with a few ESP8266 development boards such 1. Either build the circuit described below or purchase a ready built board from bbqkees. 2. Grab any ESP8266 dev board. The latest bbqkees boards have a Wemos D1 pre-mounted with a copy of this firmware. -3. Optionally add external Dallas temperature sensors and an external LED. The default pins for these are D1 and D5 respectively. -4. Decide whether to compile and upload the code yourself using PlatformIO or just upload the pre-baked firmware using the esptool (read these [instructions](#using-the-pre-built-firmware)). If you want to build yourself now is the time to customize your settings in `my_custom.h`. Upload the firmware. -5. Connect a USB 5v power supply to the ESP8266 board, either via laptop/PC or external power supply. -7. When the ESP8266 starts up for the first time the onboard LED will be flashing. This is because the EMS bus is not yet connected. +3. Optionally add external Dallas temperature sensors (to D1) and an external LED (to D5). +4. Decide whether to compile and upload the code yourself using PlatformIO or just upload the pre-baked firmware using the esptool (read these [instructions](#using-the-pre-built-firmware)). If you want to build yourself now is the time to customize your settings in `my_custom.h`. Upload the firmware via USB. +5. Connect an external USB 5v power adapter to the ESP8266 board. +7. When the ESP8266 starts up for the first time the onboard LED will be flashing. This is because the EMS bus is not yet connected and receiving data. 8. If you haven't hardcoded the WiFi credentials in step 4, the ESP8266 will boot up in a WiFi Access Point (AP) mode with the ssid name `ems-esp`. Now you can either use a laptop and connect to this AP using Telnet to `192.168.1.4` or if its powered from a computers USB use a Serial monitor tool to the ESP's COM port. Tip: to enable Telnet on Windows 10 run `dism /online /Enable-Feature /FeatureName:TelnetClient` or install something like [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). -9. Next is to change some of the settings. Type `set` to list the current stored settings. Use `set wifi` to add your wifi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the device. +9. Next is to customize some of the onboard settings. Type `set` to list the current stored settings and `?` to see the syntax. Use `set wifi` to add your wifi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the ESP. 10. The `led_gpio` will default to the onboard LED (which is probably blinking now). Ignore `thermostat_type` and `boiler_type` as these will be auto-detected hopefully later on. -11. **Important**: If `serial` is set to `on` set it to `off` using `set serial off`. The EMS bus is disabled when the serial is on. This mode is only used for setting up a new board or debugging startup issues. +11. **Important**: By default the serial port is enabled and the EMS bus disabled. This is to allow users to configure their ESP via the serial monitor when pluged into a PC/laptop. You must disable serial with `set serial off` to get the EMS transmission working. 12. Hook up the ESP to the EMS board as follows: | EMS board | ESP8266 dev board | @@ -79,10 +79,10 @@ The code and circuit has been tested with a few ESP8266 development boards such | Ground/G/J2| GND/G | | Rx/J2 | D7 | | Tx/J2 | D8 | -| VC/J2 | 3v3 or 5v | -13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5" service jack if you have an bbqkees board. +| VC/J2 | 3v3 | +13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5mm service jack if you have an bbqkees board. 14. Reboot the ESP, either by the reset switch or pulling the power. -15. The ESP will first perform an autodetect to try and discover the EMS devices attached. If your boiler and thermostat are recognized it will set these types and store them for ever and ever. You can trace the output by telnet'ing to the board `telnet ems-esp.local`. Also type `info` to check what happened. +15. The ESP will first perform an autodetect to try and discover the EMS devices attached. If your boiler and thermostat are recognized it will set these types and store them for ever and ever. You can trace the output by telnet'ing to the board `telnet ems-esp.local`. Also use `info` to check the status. 16. If your boiler/thermostat is not discovered create a GitHub issue stating the type and product ID. These will be added to the file `ems_devices.h` in a future release. 17. If all is well and there is traffic on the EMS bus the onboard LED will stop blinking and be permanently on. If this is annoying you can disable with `set led off`. To see the EMS messages type `set log v` for verbose logging. 18. And all is not well, check the wiring, make sure serial is off and look at the telnet session for errors. If in doubt, wipe the ESP with `pio run -t erase` and start again with step #3 @@ -127,8 +127,8 @@ The EMS circuit will work with both 3.3V and 5V. It's easiest though to power di - via the USB if your dev board has one - using an external 5V power supply into the 5V vin on the board -- powering from the 3.5" service jack on the boiler. This will give you 8V so you need a buck converter (like a [Pololu D24C22F5](https://www.pololu.com/product/2858)) to step this down to 5V to provide enough power to the ESP8266 (250mA at least) -- powering from the EMS line, which is 15V A/C and using a buck converter as described above. Note the current design has stability issues when sending packages in this configuration so this is not recommended yet if you plan to many send commands to the thermostat or boiler. +- powering from the 3.5mm service jack (stereo jack) on the boiler. This will give you 8V so you need a buck converter (like a [Pololu D24C22F5](https://www.pololu.com/product/2858)) to step this down to 5V to provide enough power to the ESP8266 (250mA at least) +- powering direct from the EMS line, which is 15V DC and using a buck converter as described above. | With Power Circuit | | ------------------------------------------ | From 8342947cee32b39df16d91be334baa60ed3c17ac Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 14 Mar 2019 08:36:53 +0100 Subject: [PATCH 07/59] swap uart back during ota upload --- src/emsuart.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 454ecb0fd..5fbd7fb71 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -144,6 +144,7 @@ void ICACHE_FLASH_ATTR emsuart_init() { void ICACHE_FLASH_ATTR emsuart_stop() { ETS_UART_INTR_DISABLE(); ETS_UART_INTR_ATTACH(NULL, NULL); + system_uart_swap(); // to be sure, swap Tx/Rx back. Idea from Simon Arlott } /* From 179267680c3f94051bc9dd981b79e25ad2843bc7 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 14 Mar 2019 08:37:15 +0100 Subject: [PATCH 08/59] CRASH dump support --- debug.py | 6 +- lib/MyESP/MyESP.cpp | 142 ++++++++++++++++++++++++++++---------------- lib/MyESP/MyESP.h | 53 ++++++++++++----- 3 files changed, 130 insertions(+), 71 deletions(-) diff --git a/debug.py b/debug.py index 5c8a66030..c54d05902 100644 --- a/debug.py +++ b/debug.py @@ -2,9 +2,6 @@ from subprocess import call import os -# python decoder.py -p ESP8266 -t C:\Users\Paul\.platformio\packages\toolchain-xtensa -e .pioenvs/nodemcuv2/firmware.elf stackdmp.txt -# java -jar .\EspStackTraceDecoder.jar C:\Users\Paul\.platformio\packages\toolchain-xtensa\bin\xtensa-lx106-elf-addr2line.exe .pioenvs/nodemcuv2/firmware.elf stackdmp.txt - # example stackdmp.txt would contain text like below copied & pasted from a 'crash dump' command # >>>stack>>> # 3fffff20: 3fff32f0 00000003 3fff3028 402101b2 @@ -19,4 +16,7 @@ import os # 3fffffb0: feefeffe feefeffe 3ffe8558 40100b01 # <<")); + SerialAndTelnet.println(FPSTR("* crash ")); SerialAndTelnet.println(FPSTR("* set")); SerialAndTelnet.println(FPSTR("* set wifi [ssid] [password]")); SerialAndTelnet.println(FPSTR("* set [value]")); @@ -736,8 +771,10 @@ void MyESP::_telnetCommand(char * commandLine) { } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) { char * value = _telnet_readWord(); crashTest(atoi(value)); + } else if (strcmp(cmd, "info") == 0) { + crashInfo(); } - return; + return; // don't call custom command line callback } // call callback function @@ -1171,47 +1208,38 @@ int MyESP::getWifiQuality() { * It should be kept quick / consise to be able to execute before hardware wdt may kick in */ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end) { - // Note that 'EEPROM.begin' method is reserving a RAM buffer - // The buffer size is SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_SPACE_SIZE - EEPROM.begin(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EEPROM_SIZE); - // write crash time to EEPROM uint32_t crash_time = millis(); - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); // write reset info to EEPROM - EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); - EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); // write epc1, epc2, epc3, excvaddr and depc to EEPROM - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); // write stack start and end address to EEPROM - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); - - // starting address - const uint16_t settings_start = SPI_FLASH_SEC_SIZE - SAVE_CRASH_EEPROM_SIZE - 0x10; + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); // write stack trace to EEPROM and avoid overwriting settings int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; for (uint32_t i = stack_start; i < stack_end; i++) { - if (current_address >= settings_start) - break; byte * byteValue = (byte *)i; - EEPROM.write(current_address++, *byteValue); + EEPROMr.write(current_address++, *byteValue); } - EEPROM.commit(); + EEPROMr.commit(); } void MyESP::crashTest(uint8_t t) { if (t == 1) { - myDebug("Attempting to divide by zero ..."); + myDebug("[CRASH] Attempting to divide by zero ..."); int result, zero; zero = 0; result = 1 / zero; @@ -1219,7 +1247,7 @@ void MyESP::crashTest(uint8_t t) { } if (t == 2) { - myDebug("Attempting to read through a pointer to no object ..."); + myDebug("[CRASH] Attempting to read through a pointer to no object ..."); int * nullPointer; nullPointer = NULL; // null pointer dereference - read @@ -1228,7 +1256,7 @@ void MyESP::crashTest(uint8_t t) { } if (t == 3) { - Serial.printf("Crashing with hardware WDT (%ld ms) ...\n", millis()); + Serial.printf("[CRASH] Crashing with hardware WDT (%ld ms) ...\n", millis()); ESP.wdtDisable(); while (true) { // stay in an infinite loop doing nothing @@ -1241,7 +1269,7 @@ void MyESP::crashTest(uint8_t t) { } if (t == 4) { - Serial.printf("Crashing with software WDT (%ld ms) ...\n", millis()); + Serial.printf("[CRASH] Crashing with software WDT (%ld ms) ...\n", millis()); while (true) { // stay in an infinite loop doing nothing // this way other process can not be executed @@ -1253,43 +1281,49 @@ void MyESP::crashTest(uint8_t t) { * Clears crash info */ void MyESP::crashClear() { - EEPROM.begin(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EEPROM_SIZE); - + myDebug_P(PSTR("[CRASH] Clearing crash dump")); uint32_t crash_time = 0xFFFFFFFF; - EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); - EEPROM.commit(); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + EEPROMr.commit(); +} + +/* Crash info */ +void MyESP::crashInfo() { + myDebug_P(PSTR("[EEPROM] Sector pool size: %u"), EEPROMr.size()); + myDebug_P(PSTR("[EEPROM] Sectors in use : ")); + for (uint32_t i = 0; i < EEPROMr.size(); i++) { + myDebug_P(PSTR("%d"), EEPROMr.base() - i); + } } /** * Print out crash information that has been previously saved in EEPROM */ void MyESP::crashDump() { - EEPROM.begin(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EEPROM_SIZE); - uint32_t crash_time; - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) { myDebug_P(PSTR("[CRASH] No crash info")); return; } myDebug_P(PSTR("[CRASH] Latest crash was at %lu ms after boot"), crash_time); - myDebug_P(PSTR("[CRASH] Reason of restart: %u"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON)); - myDebug_P(PSTR("[CRASH] Exception cause: %u"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE)); + myDebug_P(PSTR("[CRASH] Reason of restart: %u"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON)); + myDebug_P(PSTR("[CRASH] Exception cause: %u"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE)); uint32_t epc1, epc2, epc3, excvaddr, depc; - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc); myDebug_P(PSTR("[CRASH] epc1=0x%08x epc2=0x%08x epc3=0x%08x"), epc1, epc2, epc3); myDebug_P(PSTR("[CRASH] excvaddr=0x%08x depc=0x%08x"), excvaddr, depc); uint32_t stack_start, stack_end; - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); - EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); myDebug_P(PSTR("[CRASH] sp=0x%08x end=0x%08x"), stack_start, stack_end); @@ -1303,7 +1337,7 @@ void MyESP::crashDump() { for (int16_t i = 0; i < stack_len; i += 0x10) { SerialAndTelnet.printf("%08x: ", stack_start + i); for (byte j = 0; j < 4; j++) { - EEPROM.get(current_address, stack_trace); + EEPROMr.get(current_address, stack_trace); SerialAndTelnet.printf("%08x ", stack_trace); current_address += 4; } @@ -1313,13 +1347,16 @@ void MyESP::crashDump() { } #else void MyESP::crashTest(uint8_t t) { - myDebug("[CRASH] disabled or not supported"); + myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); } void MyESP::crashClear() { - myDebug("[CRASH] disabled or not supported"); + myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); } void MyESP::crashDump() { - myDebug("[CRASH] disabled or not supported"); + myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); +} +void MyESP::crashInfo() { + myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); } #endif @@ -1329,7 +1366,8 @@ void MyESP::begin(const char * app_hostname, const char * app_name, const char * _app_name = strdup(app_name); _app_version = strdup(app_version); - _telnet_setup(); // Telnet setup + _telnet_setup(); // Telnet setup, does first to set Serial + _eeprom_setup(); // set up eeprom for storing crash data _fs_setup(); // SPIFFS setup, do this first to get values _wifi_setup(); // WIFI setup _ota_setup(); diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 19d6ec709..2ac6ff4b2 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6b" +#define MYESP_VERSION "1.1.6c" #include #include @@ -20,9 +20,9 @@ #include // modified from https://github.com/yasheena/telnetspy #ifdef CRASH -#include "EEPROM.h" +#include extern "C" { - void custom_crash_callback(struct rst_info*, uint32_t, uint32_t); +void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); } #endif @@ -81,19 +81,36 @@ extern "C" { #define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v6/assistant/ // CRASH -#define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data -#define SAVE_CRASH_EEPROM_SIZE 0x0200 // size -#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes -#define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte -#define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte -#define SAVE_CRASH_EPC1 0x06 // 4 bytes -#define SAVE_CRASH_EPC2 0x0A // 4 bytes -#define SAVE_CRASH_EPC3 0x0E // 4 bytes -#define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes -#define SAVE_CRASH_DEPC 0x16 // 4 bytes -#define SAVE_CRASH_STACK_START 0x1A // 4 bytes -#define SAVE_CRASH_STACK_END 0x1E // 4 bytes -#define SAVE_CRASH_STACK_TRACE 0x22 // variable +#define EEPROM_ROTATE_DATA 11 // Reserved for the EEPROM_ROTATE library (3 bytes) +/** + * Structure of the single crash data set + * + * 1. Crash time + * 2. Restart reason + * 3. Exception cause + * 4. epc1 + * 5. epc2 + * 6. epc3 + * 7. excvaddr + * 8. depc + * 9. adress of stack start + * 10. adress of stack end + * 11. stack trace bytes + * ... + */ +#define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data +#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes +#define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte +#define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte +#define SAVE_CRASH_EPC1 0x06 // 4 bytes +#define SAVE_CRASH_EPC2 0x0A // 4 bytes +#define SAVE_CRASH_EPC3 0x0E // 4 bytes +#define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes +#define SAVE_CRASH_DEPC 0x16 // 4 bytes +#define SAVE_CRASH_STACK_START 0x1A // 4 bytes +#define SAVE_CRASH_STACK_END 0x1E // 4 bytes +#define SAVE_CRASH_STACK_TRACE 0x22 // variable + typedef struct { char key[40]; @@ -161,6 +178,7 @@ class MyESP { void crashClear(); void crashDump(); void crashTest(uint8_t t); + void crashInfo(); // general void end(); @@ -211,6 +229,9 @@ class MyESP { void _ota_setup(); void _OTACallback(); + // crash + void _eeprom_setup(); + // telnet & debug TelnetSpy SerialAndTelnet; void _telnetConnected(); From 0c12d2bea8669db736c919deffebd0f958f08af5 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 14 Mar 2019 08:37:54 +0100 Subject: [PATCH 09/59] revert back to core 1.8.0 libs to fix wifi resets --- platformio.ini-example | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/platformio.ini-example b/platformio.ini-example index 3687c74f0..436020466 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -1,14 +1,22 @@ [platformio] +; add here your board, e.g. nodemcuv2, d1_mini, d1_mini_pro env_default = d1_mini [common] platform = espressif8266 +[common] +platform_def = espressif8266 +platform_180 = espressif8266@1.8.0 +;platform = ${common.platform_def} +platform = ${common.platform_180} + flash_mode = dout -build_flags_debug = -ggdb3 -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH -;build_flags_prod = -Os -DBUILD_TIME=$UNIX_TIME +; for production +;build_flags = -Os -DBUILD_TIME=$UNIX_TIME -build_flags = ${common.build_flags_debug} +; for debug +build_flags = -g -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH wifi_settings = ; hard code if you prefer. Recommendation is to set from within the app when in Serial or AP mode @@ -21,6 +29,7 @@ lib_deps = AsyncMqttClient ArduinoJson OneWire + EEPROM_rotate [env:d1_mini] board = d1_mini From ebf27091ba37f56d92ad872593a0afde26a13144 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 14 Mar 2019 08:38:05 +0100 Subject: [PATCH 10/59] ignore jar files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7d0802d8a..074281300 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ platformio.ini lib/readme.txt .travis.yml -stackdmp.txt \ No newline at end of file +stackdmp.txt +*.jar \ No newline at end of file From 9cbdc2ef70ef63403933c10502dab860bfd1a6dd Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 15 Mar 2019 19:45:31 +0100 Subject: [PATCH 11/59] minor updates from Simon Arlott --- src/emsuart.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 5fbd7fb71..684900456 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -97,12 +97,12 @@ void ICACHE_FLASH_ATTR emsuart_init() { // pin settings PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U); - PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0RXD); + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD); PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0RXD_U); PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD); // set 9600, 8 bits, no parity check, 1 stop bit - USD(EMSUART_UART) = (ESP8266_CLOCK / EMSUART_BAUD); + USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD); USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1 // flush everything left over in buffer, this clears both rx and tx FIFOs @@ -145,6 +145,8 @@ void ICACHE_FLASH_ATTR emsuart_stop() { ETS_UART_INTR_DISABLE(); ETS_UART_INTR_ATTACH(NULL, NULL); system_uart_swap(); // to be sure, swap Tx/Rx back. Idea from Simon Arlott + //detachInterrupt(digitalPinToInterrupt(D7)); + //noInterrupts(); } /* From bbf2b80fb1788404556f2fbf0fd9e2e49a15187a Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 15 Mar 2019 19:45:47 +0100 Subject: [PATCH 12/59] 1.5.7 --- CHANGELOG.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f059cb5f7..e9cb81fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.7 dev] 2019-03- + +### Added + +- system command to show ESP stats +- crash command to see stack of last system crash, with .py files to track stack dump + +### Fixed + +- incorrect rendering of null temperature values (the -3200 degrees issue) +- OTA is more stable +- Added a hack to overcome WiFi power issues in esp core 2.5.0 libraries causing constant re-connects + +### Changed + +- included various fixes and suggestions from nomis (Simon Arlott) + +- upgraded MyESP library + ## [1.5.6] 2019-03-09 ### Added @@ -16,7 +35,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - upgraded MyESP library - minor changes - ## [1.5.5] 2019-03-07 ### Fixed From 5df9f14494a964f9eeab51628c7a4031934e38aa Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 15 Mar 2019 19:46:11 +0100 Subject: [PATCH 13/59] 1.5.7 - fix for core 2.5.0 and wifi issues --- lib/MyESP/MyESP.cpp | 149 ++++++++++++++++++++++++++++------------- lib/MyESP/MyESP.h | 12 ++-- platformio.ini-example | 4 +- src/ems-esp.ino | 28 +++++--- src/ems.cpp | 10 ++- src/version.h | 2 +- 6 files changed, 137 insertions(+), 68 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index cccb28dce..f578b0751 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -1,5 +1,5 @@ /* - * MyESP - my ESP helper class to handle Wifi, MQTT and Telnet + * MyESP - my ESP helper class to handle WiFi, MQTT and Telnet * * Paul Derbyshire - December 2018 * @@ -26,7 +26,7 @@ MyESP::MyESP() { _app_name = strdup("MyESP"); _app_version = strdup(MYESP_VERSION); - _boottime = strdup(""); + _boottime = NULL; _load_average = 100; // calculated load average _telnetcommand_callback = NULL; @@ -314,6 +314,8 @@ void MyESP::_wifi_setup() { jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm jw.cleanNetworks(); // Clean existing network configuration jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network + + WiFi.setSleepMode(WIFI_NONE_SLEEP); // TODO: possible fix for wifi dropouts in core 2.5.0 } // set the callback function for the OTA onstart @@ -381,7 +383,6 @@ void MyESP::_ota_setup() { // There's been an error, reenable rotation EEPROMr.rotate(true); #endif - }); } @@ -498,47 +499,27 @@ void MyESP::_consoleShowHelp() { SerialAndTelnet.println(); if (WiFi.getMode() & WIFI_AP) { - SerialAndTelnet.printf("* ESP is in AP mode with SSID %s", jw.getAPSSID().c_str()); - SerialAndTelnet.println(); + SerialAndTelnet.printf("* Device is in AP mode with SSID %s", jw.getAPSSID().c_str()); } else { -#if defined(ARDUINO_ARCH_ESP32) - String hostname = String(WiFi.getHostname()); -#else - String hostname = WiFi.hostname(); -#endif - SerialAndTelnet.printf("* Hostname: %s IP: %s MAC: %s", - hostname.c_str(), - WiFi.localIP().toString().c_str(), - WiFi.macAddress().c_str()); -#ifdef ARDUINO_BOARD - SerialAndTelnet.printf(" Board: %s", ARDUINO_BOARD); -#endif - SerialAndTelnet.printf(" (MyESP v%s)", MYESP_VERSION); - -#ifdef BUILD_TIME - SerialAndTelnet.print(" (Build "); - _printBuildTime(BUILD_TIME); - SerialAndTelnet.print(")"); -#endif + SerialAndTelnet.printf("* Hostname: %s (%s)", getESPhostname().c_str(), WiFi.localIP().toString().c_str()); SerialAndTelnet.println(); - SerialAndTelnet.printf("* Connected to WiFi SSID: %s (signal %d%%)", WiFi.SSID().c_str(), getWifiQuality()); + SerialAndTelnet.printf("* WiFi SSID: %s (signal %d%%)", WiFi.SSID().c_str(), getWifiQuality()); SerialAndTelnet.println(); SerialAndTelnet.printf("* MQTT is %s", mqttClient.connected() ? "connected" : "disconnected"); - SerialAndTelnet.println(); + } + + SerialAndTelnet.println(); + + if (_boottime != NULL) { SerialAndTelnet.printf("* Boot time: %s", _boottime); SerialAndTelnet.println(); } - SerialAndTelnet.printf("* Free RAM: %d KB Load: %d%%", (ESP.getFreeHeap() / 1024), getSystemLoadAverage()); - SerialAndTelnet.println(); - // for battery power is ESP.getVcc() - SerialAndTelnet.println(FPSTR("*")); SerialAndTelnet.println(FPSTR("* Commands:")); - SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit")); - SerialAndTelnet.println(FPSTR("* reboot")); - SerialAndTelnet.println(FPSTR("* crash ")); - SerialAndTelnet.println(FPSTR("* set")); + SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit telnet")); + SerialAndTelnet.println(FPSTR("* set | reboot | system")); + SerialAndTelnet.println(FPSTR("* crash ")); SerialAndTelnet.println(FPSTR("* set wifi [ssid] [password]")); SerialAndTelnet.println(FPSTR("* set [value]")); SerialAndTelnet.println(FPSTR("* set erase")); @@ -761,6 +742,13 @@ void MyESP::_telnetCommand(char * commandLine) { resetESP(); } + // show system stats + if ((strcmp(ptrToCommandName, "system") == 0) && (wc == 1)) { + showSystemStats(); + return; + } + + // crash command if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { char * cmd = _telnet_readWord(); @@ -771,8 +759,6 @@ void MyESP::_telnetCommand(char * commandLine) { } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) { char * value = _telnet_readWord(); crashTest(atoi(value)); - } else if (strcmp(cmd, "info") == 0) { - crashInfo(); } return; // don't call custom command line callback } @@ -781,6 +767,82 @@ void MyESP::_telnetCommand(char * commandLine) { (_telnetcommand_callback)(wc, commandLine); } +// returns WiFi hostname as a String object +String MyESP::getESPhostname() { + String hostname; + +#if defined(ARDUINO_ARCH_ESP32) + hostname = String(WiFi.getHostname()); +#else + hostname = WiFi.hostname(); +#endif + + return (hostname); +} + +// print out ESP system stats +// for battery power is ESP.getVcc() +void MyESP::showSystemStats() { +#ifdef BUILD_TIME + myDebug_P("[SYSTEM] Build timestamp: %s)", _printBuildTime(BUILD_TIME);); +#endif + + myDebug_P(PSTR("[APP] MyESP version: %s"), MYESP_VERSION); + myDebug_P(PSTR("[APP] System Load: %d%%"), getSystemLoadAverage()); + + if (WiFi.getMode() & WIFI_AP) { + myDebug_P(PSTR("[WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + } else { + myDebug_P(PSTR("[WIFI] Wifi Hostname: %s"), getESPhostname().c_str()); + myDebug_P(PSTR("[WIFI] Wifi IP: %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] Wifi signal strength: %d%%"), getWifiQuality()); + } + + myDebug_P(PSTR("[WIFI] Wifi MAC: %s"), WiFi.macAddress().c_str()); + +#ifdef CRASH + char output_str[80] = {0}; + char buffer[16] = {0}; + /* Crash info */ + myDebug_P(PSTR("[EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); + strlcpy(output_str, PSTR("[EEPROM] EEPROM Sector pool size is "), sizeof(output_str)); + strlcat(output_str, itoa(EEPROMr.size(), buffer, 10), sizeof(output_str)); + strlcat(output_str, PSTR(", and in use are: "), sizeof(output_str)); + for (uint32_t i = 0; i < EEPROMr.size(); i++) { + strlcat(output_str, itoa(EEPROMr.base() - i, buffer, 10), sizeof(output_str)); + strlcat(output_str, PSTR(" "), sizeof(output_str)); + } + myDebug_P(output_str); +#endif + +#ifdef ARDUINO_BOARD + myDebug_P(PSTR("[SYSTEM] Board: %s"), ARDUINO_BOARD); +#endif + + myDebug_P(PSTR("[SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); + myDebug_P(PSTR("[SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); + myDebug_P(PSTR("[SYSTEM] SDK version: %s"), ESP.getSdkVersion()); + myDebug_P(PSTR("[SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); + myDebug_P(PSTR("[SYSTEM] Boot version: %d"), ESP.getBootVersion()); + myDebug_P(PSTR("[SYSTEM] Boot mode: %d"), ESP.getBootMode()); + //myDebug_P(PSTR("[SYSTEM] Firmware MD5: %s"), (char *)ESP.getSketchMD5().c_str()); + + FlashMode_t mode = ESP.getFlashChipMode(); + myDebug_P(PSTR("[FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); + myDebug_P(PSTR("[FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); + myDebug_P(PSTR("[FLASH] Flash mode: %s"), + mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); + myDebug_P(PSTR("[FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); + myDebug_P(PSTR("[FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); + myDebug_P(PSTR("[FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR("[MEM] Firmware size: %d"), ESP.getSketchSize()); + myDebug_P(PSTR("[MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); + myDebug_P(PSTR("[MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR("[MEM] Free Heap: %d"), ESP.getFreeHeap()); + myDebug_P(PSTR("[MEM] Stack: %d"), ESP.getFreeContStack()); +} + + // handler for Telnet void MyESP::_telnetHandle() { SerialAndTelnet.handle(); @@ -1134,7 +1196,7 @@ void MyESP::_fs_setup() { fs_saveConfig(); } - //_fs_printConfig(); // TODO: for debugging + //_fs_printConfig(); // enable for debugging } uint16_t MyESP::getSystemLoadAverage() { @@ -1287,15 +1349,6 @@ void MyESP::crashClear() { EEPROMr.commit(); } -/* Crash info */ -void MyESP::crashInfo() { - myDebug_P(PSTR("[EEPROM] Sector pool size: %u"), EEPROMr.size()); - myDebug_P(PSTR("[EEPROM] Sectors in use : ")); - for (uint32_t i = 0; i < EEPROMr.size(); i++) { - myDebug_P(PSTR("%d"), EEPROMr.base() - i); - } -} - /** * Print out crash information that has been previously saved in EEPROM */ @@ -1378,14 +1431,16 @@ void MyESP::begin(const char * app_hostname, const char * app_name, const char * */ void MyESP::loop() { _calculateLoad(); - _telnetHandle(); // Telnet/Debugger + _telnetHandle(); jw.loop(); // WiFi + /* // do nothing else until we've got a wifi connection if (WiFi.getMode() & WIFI_AP) { return; } + */ ArduinoOTA.handle(); // OTA _mqttConnect(); // MQTT diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 2ac6ff4b2..2c4b2078a 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6c" +#define MYESP_VERSION "1.1.6d" #include #include @@ -75,13 +75,13 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); #define COLOR_CYAN "\x1B[0;36m" #define COLOR_WHITE "\x1B[0;37m" #define COLOR_BOLD_ON "\x1B[1m" -#define COLOR_BOLD_OFF "\x1B[22m" // fixed by Scott Arlott +#define COLOR_BOLD_OFF "\x1B[22m" // fix by Scott Arlott to support Linux // SPIFFS #define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v6/assistant/ // CRASH -#define EEPROM_ROTATE_DATA 11 // Reserved for the EEPROM_ROTATE library (3 bytes) +#define EEPROM_ROTATE_DATA 11 // Reserved for the EEPROM_ROTATE library (3 bytes) /** * Structure of the single crash data set * @@ -93,8 +93,8 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); * 6. epc3 * 7. excvaddr * 8. depc - * 9. adress of stack start - * 10. adress of stack end + * 9. address of stack start + * 10. address of stack end * 11. stack trace bytes * ... */ @@ -188,6 +188,7 @@ class MyESP { void resetESP(); uint16_t getSystemLoadAverage(); int getWifiQuality(); + void showSystemStats(); private: @@ -223,6 +224,7 @@ class MyESP { char * _wifi_ssid; char * _wifi_password; bool _wifi_connected; + String getESPhostname(); // ota ota_callback_f _ota_callback; diff --git a/platformio.ini-example b/platformio.ini-example index 436020466..50f45d778 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -7,8 +7,8 @@ platform = espressif8266 [common] platform_def = espressif8266 platform_180 = espressif8266@1.8.0 -;platform = ${common.platform_def} -platform = ${common.platform_180} +platform = ${common.platform_def} +;platform = ${common.platform_180} flash_mode = dout diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 3ef90cd38..634de71a0 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -251,6 +251,8 @@ void _renderBoolValue(const char * prefix, uint8_t value) { void showInfo() { // General stats from EMS bus + char buffer_type[128] = {0}; + myDebug("%sEMS-ESP System stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); _EMS_SYS_LOGGING sysLog = ems_getLogging(); if (sysLog == EMS_SYS_LOGGING_BASIC) { @@ -263,9 +265,7 @@ void showInfo() { myDebug(" System logging set to None"); } - myDebug(" LED is %s", EMSESP_Status.led_enabled ? "on" : "off"); - myDebug(" Test Mode is %s", EMSESP_Status.test_mode ? "on" : "off"); - + myDebug(" LED is %s, Test Mode is %s", EMSESP_Status.led_enabled ? "on" : "off", EMSESP_Status.test_mode ? "on" : "off"); myDebug(" # connected Dallas temperature sensors=%d", EMSESP_Status.dallas_sensors); myDebug(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s", @@ -286,19 +286,25 @@ void showInfo() { myDebug("%sBoiler stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); // version details - char buffer_type[64]; myDebug(" Boiler type: %s", ems_getBoilerDescription(buffer_type)); // active stats if (ems_getBusConnected()) { - myDebug(" Hot tap water is %s", (EMS_Boiler.tapwaterActive ? "running" : "off")); - myDebug(" Central Heating is %s", (EMS_Boiler.heatingActive ? "active" : "off")); + if (EMS_Boiler.tapwaterActive != EMS_VALUE_INT_NOTSET) { + myDebug(" Hot tap water is %s", EMS_Boiler.tapwaterActive ? "running" : "off"); + } + + if (EMS_Boiler.heatingActive != EMS_VALUE_INT_NOTSET) { + myDebug(" Central Heating is %s", EMS_Boiler.heatingActive ? "active" : "off"); + } } // UBAParameterWW _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); - myDebug(" Warm Water is set to %s", (EMS_Boiler.wWComfort ? "Comfort" : "ECO")); + if (EMS_Boiler.wWComfort != EMS_VALUE_INT_NOTSET) { + myDebug(" Warm Water is set to %s", (EMS_Boiler.wWComfort ? "Comfort" : "ECO")); + } _renderIntValue("Warm Water selected temperature", "C", EMS_Boiler.wWSelTemp); _renderIntValue("Warm Water desired temperature", "C", EMS_Boiler.wWDesiredTemp); @@ -392,9 +398,9 @@ void showInfo() { // Dallas if (EMSESP_Status.dallas_sensors != 0) { + char s[80] = {0}; myDebug("%sExternal temperature sensors:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); - for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { - char s[80] = {0}; + for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { snprintf(s, sizeof(s), "Sensor #%d", i + 1); _renderFloatValue(s, "C", ds18.getValue(i)); } @@ -1066,7 +1072,7 @@ void showerCheck() { // if already in cold mode, ignore all this logic until we're out of the cold blast if (!EMSESP_Shower.doingColdShot) { // is the hot water running? - if (EMS_Boiler.tapwaterActive) { + if (EMS_Boiler.tapwaterActive == 1) { // if heater was previously off, start the timer if (EMSESP_Shower.timerStart == 0) { // hot water just started... @@ -1209,4 +1215,6 @@ void loop() { if (EMSESP_Status.shower_timer) { showerCheck(); } + + delay(1); // some time to WiFi and everything else to catch up } diff --git a/src/ems.cpp b/src/ems.cpp index 8818010f7..8d7d93f74 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -872,11 +872,15 @@ void _processType(uint8_t * telegram, uint8_t length) { * using a quick hack for checking the heating. Selected Flow Temp >= 70 */ void _checkActive() { - // hot tap water, using flow to check insread of the burner power - EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + // hot tap water, using flow to check instead of the burner power + if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET && EMS_Boiler.burnGas != EMS_VALUE_INT_NOTSET) { + EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + } // heating - EMS_Boiler.heatingActive = ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET && EMS_Boiler.burnGas != EMS_VALUE_INT_NOTSET) { + EMS_Boiler.heatingActive = ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + } } /** diff --git a/src/version.h b/src/version.h index 0eed2b101..5b7e1213c 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.5.7b" +#define APP_VERSION "1.5.7c" #define APP_HOSTNAME "ems-esp" From d3885f735deb5a51077043e64e89dc263c97a017 Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 15 Mar 2019 21:12:00 +0100 Subject: [PATCH 14/59] merged in Dallas changes from JewelZB --- CHANGELOG.md | 1 + src/ds18.cpp | 13 +++++------ src/ds18.h | 9 ++++---- src/ems-esp.ino | 59 ++++++++++++++++++++++++++++++++++++++++++++++++- src/my_config.h | 4 ++++ 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9cb81fdc..0ebf296a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - system command to show ESP stats - crash command to see stack of last system crash, with .py files to track stack dump +- publish dallas external temp sensors to MQTT (Thanks @JewelZB) ### Fixed diff --git a/src/ds18.cpp b/src/ds18.cpp index a95c60441..e3607a15a 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -25,10 +25,11 @@ DS18::~DS18() { } // init -uint8_t DS18::setup(uint8_t gpio) { +uint8_t DS18::setup(uint8_t gpio, bool parasite) { uint8_t count; - _gpio = gpio; + _gpio = gpio; + _parasite = (parasite ? 1 : 0); // OneWire if (_wire) @@ -62,8 +63,7 @@ void DS18::loop() { // Start conversion _wire->reset(); _wire->skip(); - _wire->write(DS18_CMD_START_CONVERSION, DS18_PARASITE); - + _wire->write(DS18_CMD_START_CONVERSION, _parasite); } else { // Read scratchpads for (unsigned char index = 0; index < _devices.size(); index++) { @@ -161,7 +161,7 @@ double DS18::getValue(unsigned char index) { uint8_t * data = _devices[index].data; if (OneWire::crc8(data, DS18_DATA_SIZE - 1) != data[DS18_DATA_SIZE - 1]) { - return 0; + return DS18_CRC_ERROR; } int16_t raw = (data[1] << 8) | data[0]; @@ -182,9 +182,6 @@ double DS18::getValue(unsigned char index) { } double value = (float)raw / 16.0; - if (value == DS18_DISCONNECTED) { - return 0; - } return value; } diff --git a/src/ds18.h b/src/ds18.h index d4dd9cfeb..db7c78758 100644 --- a/src/ds18.h +++ b/src/ds18.h @@ -20,8 +20,8 @@ #define DS18_CHIP_DS1825 0x3B #define DS18_DATA_SIZE 9 -#define DS18_PARASITE 1 #define DS18_DISCONNECTED -127 +#define DS18_CRC_ERROR -126 #define GPIO_NONE 0x99 #define DS18_READ_INTERVAL 2000 // Force sensor read & cache every 2 seconds @@ -39,7 +39,7 @@ class DS18 { DS18(); ~DS18(); - uint8_t setup(uint8_t gpio); + uint8_t setup(uint8_t gpio, bool parasite); void loop(); char * getDeviceString(char * s, unsigned char index); double getValue(unsigned char index); @@ -50,6 +50,7 @@ class DS18 { uint8_t loadDevices(); OneWire * _wire; - uint8_t _count; // # devices - uint8_t _gpio; // the sensor pin + uint8_t _count; // # devices + uint8_t _gpio; // the sensor pin + uint8_t _parasite; // parasite mode }; diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 634de71a0..43c521f3a 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -35,6 +35,9 @@ DS18 ds18; #define PUBLISHVALUES_TIME 120 // every 2 minutes publish MQTT values Ticker publishValuesTimer; +#define PUBLISHSENSORVALUES_TIME 180 // every 3 minutes publish MQTT sensor values +Ticker publishSensorValuesTimer; + #define SYSTEMCHECK_TIME 20 // every 20 seconds check if Boiler is online Ticker systemCheckTimer; @@ -67,6 +70,7 @@ typedef struct { uint8_t dallas_sensors; // count of dallas sensors uint8_t led_gpio; uint8_t dallas_gpio; + uint8_t dallas_parasite; } _EMSESP_Status; typedef struct { @@ -414,6 +418,32 @@ void showInfo() { } } +// send all dallas sensor values as a JSON package to MQTT +void publishSensorValues() { + StaticJsonDocument doc; + bool hasdata = false; + + // see if the sensor values have changed, if so send + //JsonObject & sensors = jsonBuffer.createObject(); + JsonObject sensors = doc.to(); + for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { + double sensorValue = ds18.getValue(i); + if(sensorValue != DS18_DISCONNECTED && sensorValue != DS18_CRC_ERROR) { + char label[8] = {0}; + char valuestr[8] = {0}; // for formatting temp + sprintf(label,"temp_%d",(i+1)); + sensors[label] = _float_to_char(valuestr, sensorValue); + hasdata = true; + } + } + + if (hasdata) { + char data[MQTT_MAX_SIZE] = {0}; + serializeJson(doc, data, sizeof(data)); + myESP.mqttPublish(TOPIC_EXTERNAL_SENSORS, data); + } +} + // send values via MQTT // a json object is created for the boiler and one for the thermostat // CRC check is done to see if there are changes in the values since the last send to avoid too much wifi traffic @@ -607,6 +637,11 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; // default value } + // dallas_gpio + if (!(EMSESP_Status.dallas_gpio = json["dallas_parasite"])) { + EMSESP_Status.dallas_parasite = EMSESP_DALLAS_PARASITE; // default value + } + // thermostat_type if (!(EMS_Thermostat.type_id = json["thermostat_type"])) { EMS_Thermostat.type_id = EMSESP_THERMOSTAT_TYPE; // set default @@ -629,6 +664,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { json["led"] = EMSESP_Status.led_enabled; json["led_gpio"] = EMSESP_Status.led_gpio; json["dallas_gpio"] = EMSESP_Status.dallas_gpio; + json["dallas_parasite"] = EMSESP_Status.dallas_parasite; json["thermostat_type"] = EMS_Thermostat.type_id; json["boiler_type"] = EMS_Boiler.type_id; json["test_mode"] = EMSESP_Status.test_mode; @@ -686,6 +722,17 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c ok = true; } + // dallas_parasite + if ((strcmp(setting, "dallas_parasite") == 0) && (wc == 2)) { + if (strcmp(value, "true") == 0) { + EMSESP_Status.dallas_parasite = true; + ok = true; + } else if (strcmp(value, "false") == 0) { + EMSESP_Status.dallas_parasite = false; + ok = true; + } + } + // thermostat_type if (strcmp(setting, "thermostat_type") == 0) { EMS_Thermostat.type_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); @@ -704,6 +751,7 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug(" led=%s", EMSESP_Status.led_enabled ? "on" : "off"); myDebug(" led_gpio=%d", EMSESP_Status.led_gpio); myDebug(" dallas_gpio=%d", EMSESP_Status.dallas_gpio); + myDebug(" dallas_parasite=%s", EMSESP_Status.dallas_parasite ? "on" : "off"); if (EMS_Thermostat.type_id == EMS_ID_NONE) { myDebug(" thermostat_type="); @@ -997,6 +1045,13 @@ void initEMSESP() { EMSESP_Shower.doingColdShot = false; } +// publish external dallas sensor temperature values to MQTT +void do_publishSensorValues() { + if (EMSESP_Status.dallas_sensors != 0) { + publishSensorValues(); + } +} + // call PublishValues without forcing, so using CRC to see if we really need to publish void do_publishValues() { // don't publish if we're not connected to the EMS bus @@ -1177,6 +1232,7 @@ void setup() { if (!EMSESP_Status.test_mode) { publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post MQTT values regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS + publishSensorValuesTimer.attach(PUBLISHSENSORVALUES_TIME, do_publishSensorValues); // post MQTT sensor values } // set pin for LED @@ -1187,7 +1243,8 @@ void setup() { } // check for Dallas sensors - EMSESP_Status.dallas_sensors = ds18.setup(EMSESP_Status.dallas_gpio); // returns #sensors + EMSESP_Status.dallas_sensors = ds18.setup(EMSESP_Status.dallas_gpio, EMSESP_Status.dallas_parasite); // returns #sensors + } // diff --git a/src/my_config.h b/src/my_config.h index b9300c250..3fea91258 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -49,6 +49,9 @@ #define BOILER_SHOWER_ALERT 0 // enable (1) to send alert of cold water when shower time limit has exceeded #define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water +// MQTT for EXTERNAL SENSORS +#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT + //////////////////////////////////////////////////////////////////////////////////////////////////// // THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // // ALTHOUGH YOU MAY ALSO HARDCODE THEM HERE BUT THEY WILL BE OVERWRITTEN WITH NEW RELEASE UPDATES // @@ -64,6 +67,7 @@ // set this if using an external temperature sensor like a DS18B20 // D5 is the default on bbqkees' board #define EMSESP_DALLAS_GPIO D5 +#define EMSESP_DALLAS_PARASITE false // By default the EMS bus will be scanned for known devices based on product ids in ems_devices.h // You can override the Thermostat and Boiler types here From 90b278bbf796f6d81c40f177048bdcd91a2af311 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 16 Mar 2019 12:50:36 +0100 Subject: [PATCH 15/59] moved shower to settings --- src/my_config.h | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/my_config.h b/src/my_config.h index 3fea91258..6248f478d 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -44,32 +44,24 @@ #define TOPIC_SHOWER_ALERT "shower_alert" // toggle switch for enabling the shower alarm logic #define TOPIC_SHOWER_COLDSHOT "shower_coldshot" // used to trigger a coldshot from an MQTT command -// default values for shower logic on/off -#define BOILER_SHOWER_TIMER 0 // enable (1) to monitor shower time -#define BOILER_SHOWER_ALERT 0 // enable (1) to send alert of cold water when shower time limit has exceeded -#define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water - // MQTT for EXTERNAL SENSORS -#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT +#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT //////////////////////////////////////////////////////////////////////////////////////////////////// // THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // -// ALTHOUGH YOU MAY ALSO HARDCODE THEM HERE BUT THEY WILL BE OVERWRITTEN WITH NEW RELEASE UPDATES // //////////////////////////////////////////////////////////////////////////////////////////////////// -// Set LED pin used for showing ems bus connection status. Solid is connected, Flashing is error -// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED -// (e.g. D1 on a bbqkees' board -// can be enabled and disabled via the 'set led' -// pin can be set by 'set led_gpio' +// Set LED pin used for showing the EMS bus connection status. Solid means EMS bus working, flashing is an error +// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED (e.g. D1 on a bbqkees' board) +// can be enabled and disabled via the 'set led' command and pin set by 'set led_gpio' #define EMSESP_LED_GPIO LED_BUILTIN // set this if using an external temperature sensor like a DS18B20 -// D5 is the default on bbqkees' board +// D5 is the default on a bbqkees board #define EMSESP_DALLAS_GPIO D5 #define EMSESP_DALLAS_PARASITE false -// By default the EMS bus will be scanned for known devices based on product ids in ems_devices.h +// By default the EMS bus will be scanned for known devices based on the product ids in ems_devices.h // You can override the Thermostat and Boiler types here #define EMSESP_BOILER_TYPE EMS_ID_NONE #define EMSESP_THERMOSTAT_TYPE EMS_ID_NONE From 29d896abf62b412cd63db289ed7268c368c79a90 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 16 Mar 2019 12:50:53 +0100 Subject: [PATCH 16/59] removed BUILD TIME --- platformio.ini-example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini-example b/platformio.ini-example index 50f45d778..8a13925c5 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -13,7 +13,7 @@ platform = ${common.platform_def} flash_mode = dout ; for production -;build_flags = -Os -DBUILD_TIME=$UNIX_TIME +;build_flags = -Os ; for debug build_flags = -g -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH From 7dffcfadc65ad2d28c0042bec6bf689bc17a8215 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 16 Mar 2019 12:51:10 +0100 Subject: [PATCH 17/59] added parasite --- src/ds18.cpp | 12 +++++------- src/ds18.h | 3 --- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/ds18.cpp b/src/ds18.cpp index e3607a15a..563e5c30d 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -4,9 +4,6 @@ * * Paul Derbyshire - https://github.com/proddy/EMS-ESP * - * See ChangeLog.md for history - * See README.md for Acknowledgments - * */ #include "ds18.h" @@ -14,9 +11,10 @@ std::vector _devices; DS18::DS18() { - _wire = NULL; - _count = 0; - _gpio = GPIO_NONE; + _wire = NULL; + _count = 0; + _gpio = GPIO_NONE; + _parasite = 0; } DS18::~DS18() { @@ -117,7 +115,7 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { char a[30] = {0}; snprintf(a, sizeof(a), - "(%02X%02X%02X%02X%02X%02X%02X%02X) @ GPIO%d", + " (%02X%02X%02X%02X%02X%02X%02X%02X) @ GPIO%d", address[0], address[1], address[2], diff --git a/src/ds18.h b/src/ds18.h index db7c78758..a537ecf87 100644 --- a/src/ds18.h +++ b/src/ds18.h @@ -4,9 +4,6 @@ * * Paul Derbyshire - https://github.com/proddy/EMS-ESP * - * See ChangeLog.md for history - * See README.md for Acknowledgments - * */ #pragma once From a5103513f2fe6ad7a3ada85eff97c079abf2fee1 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 16 Mar 2019 12:51:33 +0100 Subject: [PATCH 18/59] warm water modes --- src/ems-esp.ino | 183 ++++++++++++++++++++++++++++++---------------- src/ems.cpp | 31 ++++---- src/ems.h | 2 +- src/ems_devices.h | 11 +-- 4 files changed, 146 insertions(+), 81 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 43c521f3a..7f1bc9b21 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -59,18 +59,20 @@ Ticker showerColdShotStopTimer; #define SHOWER_MIN_DURATION 120000 // in ms. 2 minutes, before recognizing its a shower #define SHOWER_OFFSET_TIME 5000 // in ms. 5 seconds grace time, to calibrate actual time under the shower #define SHOWER_COLDSHOT_DURATION 10 // in seconds. 10 seconds for cold water before turning back hot water +#define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water typedef struct { - bool shower_timer; // true if we want to report back on shower times - bool shower_alert; // true if we want the alert of cold water - bool led_enabled; // LED on/off - bool test_mode; // test mode to stop automatic Tx on/off - unsigned long timestamp; // for internal timings, via millis() uint8_t dallas_sensors; // count of dallas sensors - uint8_t led_gpio; - uint8_t dallas_gpio; - uint8_t dallas_parasite; + + // custom params + bool shower_timer; // true if we want to report back on shower times + bool shower_alert; // true if we want the alert of cold water + bool led_enabled; // LED on/off + bool test_mode; // test mode to stop automatic Tx on/off + uint8_t led_gpio; + uint8_t dallas_gpio; + uint8_t dallas_parasite; } _EMSESP_Status; typedef struct { @@ -83,27 +85,30 @@ typedef struct { command_t PROGMEM project_cmds[] = { - {"set led ", "toggle status LED on/off"}, - {"set led_gpio ", "set the LED pin. Default is the onboard LED (D1=5)"}, - {"set dallas_gpio ", "set the pin for external Dallas temperature sensors (D5=14)"}, - {"set thermostat_type ", "set the thermostat type id (e.g. 10 for 0x10)"}, - {"set boiler_type ", "set the boiler type id (e.g. 8 for 0x08)"}, - {"set test_mode ", "test_mode turns off all automatic reads"}, - {"info", "show data captured on the EMS bus"}, - {"log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, - {"publish", "publish all values to MQTT"}, - {"types", "list supported EMS telegram type IDs"}, - {"queue", "show current Tx queue"}, - {"autodetect", "discover EMS devices and attempt to automatically set boiler and thermostat"}, - {"shower ", "toggle either timer or alert on/off"}, - {"send XX ...", "send raw telegram data as hex to EMS bus"}, - {"thermostat read ", "send read request to the thermostat"}, - {"thermostat temp ", "set current thermostat temperature"}, - {"thermostat mode ", "set mode (0=low/night, 1=manual/day, 2=auto)"}, - {"thermostat scan ", "do a read on all type IDs"}, - {"boiler read ", "send read request to boiler"}, - {"boiler wwtemp ", "set boiler warm water temperature"}, - {"boiler tapwater ", "set boiler warm tap water on/off"} + {true, "led ", "toggle status LED on/off"}, + {true, "led_gpio ", "set the LED pin. Default is the onboard LED (D1=5)"}, + {true, "dallas_gpio ", "set the pin for external Dallas temperature sensors (D5=14)"}, + {true, "dallas_parasite ", "set to on if powering Dallas via parasite"}, + {true, "thermostat_type ", "set the thermostat type id (e.g. 10 for 0x10)"}, + {true, "boiler_type ", "set the boiler type id (e.g. 8 for 0x08)"}, + {true, "test_mode ", "test_mode turns off all automatic reads"}, + {true, "shower_timer ", "notify on shower durations"}, + {true, "shower_alert ", "send a warning of cold water after shower time is exceeded"}, + {false, "info", "show data captured on the EMS bus"}, + {false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, + {false, "publish", "publish all values to MQTT"}, + {false, "types", "list supported EMS telegram type IDs"}, + {false, "queue", "show current Tx queue"}, + {false, "autodetect", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, + {false, "shower ", "toggle either timer or alert on/off"}, + {false, "send XX ...", "send raw telegram data as hex to EMS bus"}, + {false, "thermostat read ", "send read request to the thermostat"}, + {false, "thermostat temp ", "set current thermostat temperature"}, + {false, "thermostat mode ", "set mode (0=low/night, 1=manual/day, 2=auto)"}, + {false, "thermostat scan ", "do a read on all type IDs"}, + {false, "boiler read ", "send read request to boiler"}, + {false, "boiler wwtemp ", "set boiler warm water temperature"}, + {false, "boiler tapwater ", "set boiler warm tap water on/off"} }; @@ -306,9 +311,14 @@ void showInfo() { // UBAParameterWW _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); - if (EMS_Boiler.wWComfort != EMS_VALUE_INT_NOTSET) { - myDebug(" Warm Water is set to %s", (EMS_Boiler.wWComfort ? "Comfort" : "ECO")); + if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { + myDebug(" Warm Water comfort is set to Hot"); + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { + myDebug(" Warm Water comfort is set to Eco"); + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { + myDebug(" Warm Water comfort is set to Intelligent"); } + _renderIntValue("Warm Water selected temperature", "C", EMS_Boiler.wWSelTemp); _renderIntValue("Warm Water desired temperature", "C", EMS_Boiler.wWDesiredTemp); @@ -337,7 +347,11 @@ void showInfo() { _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); - myDebug(" System Service Code: %s (%d)", EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); + if (EMS_Boiler.serviceCode == EMS_VALUE_SHORT_NOTSET) { + myDebug(" System service code: %s", EMS_Boiler.serviceCodeChar); + } else { + myDebug(" System service code: %s (%d)", EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); + } // UBAParametersMessage _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); @@ -402,10 +416,11 @@ void showInfo() { // Dallas if (EMSESP_Status.dallas_sensors != 0) { - char s[80] = {0}; + char s[80] = {0}; + char buffer[128] = {0}; myDebug("%sExternal temperature sensors:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); - for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { - snprintf(s, sizeof(s), "Sensor #%d", i + 1); + for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { + snprintf(s, sizeof(s), "Sensor #%d %s", i + 1, ds18.getDeviceString(buffer, i)); _renderFloatValue(s, "C", ds18.getValue(i)); } myDebug(""); // newline @@ -421,19 +436,18 @@ void showInfo() { // send all dallas sensor values as a JSON package to MQTT void publishSensorValues() { StaticJsonDocument doc; - bool hasdata = false; + bool hasdata = false; - // see if the sensor values have changed, if so send - //JsonObject & sensors = jsonBuffer.createObject(); + // see if the sensor values have changed, if so send JsonObject sensors = doc.to(); for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { double sensorValue = ds18.getValue(i); - if(sensorValue != DS18_DISCONNECTED && sensorValue != DS18_CRC_ERROR) { - char label[8] = {0}; - char valuestr[8] = {0}; // for formatting temp - sprintf(label,"temp_%d",(i+1)); + if (sensorValue != DS18_DISCONNECTED && sensorValue != DS18_CRC_ERROR) { + char label[8] = {0}; + char valuestr[8] = {0}; // for formatting temp + sprintf(label, "temp_%d", (i + 1)); sensors[label] = _float_to_char(valuestr, sensorValue); - hasdata = true; + hasdata = true; } } @@ -455,8 +469,8 @@ void publishValues(bool force) { uint32_t fchecksum; static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off - static uint32_t previousBoilerPublishCRC = 0; // CRC check - static uint32_t previousThermostatPublishCRC = 0; // CRC check + static uint32_t previousBoilerPublishCRC = 0; // CRC check for boiler values + static uint32_t previousThermostatPublishCRC = 0; // CRC check for thermostat values JsonObject rootBoiler = doc.to(); @@ -464,8 +478,16 @@ void publishValues(bool force) { rootBoiler["selFlowTemp"] = _float_to_char(s, EMS_Boiler.selFlowTemp); rootBoiler["outdoorTemp"] = _float_to_char(s, EMS_Boiler.extTemp); rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); - rootBoiler["wWComfort"] = EMS_Boiler.wWComfort ? "Comfort" : "ECO"; - rootBoiler["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); + + if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { + rootBoiler["wWComfort"] = "Hot"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { + rootBoiler["wWComfort"] = "Eco"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { + rootBoiler["wWComfort"] = "Intelligent"; + } + + rootBoiler["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); snprintf(s, sizeof(s), "%i.%i", EMS_Boiler.wWCurFlow / 10, EMS_Boiler.wWCurFlow % 10); rootBoiler["wWCurFlow"] = s; rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); @@ -637,8 +659,8 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; // default value } - // dallas_gpio - if (!(EMSESP_Status.dallas_gpio = json["dallas_parasite"])) { + // dallas_parasite + if (!(EMSESP_Status.dallas_parasite = json["dallas_parasite"])) { EMSESP_Status.dallas_parasite = EMSESP_DALLAS_PARASITE; // default value } @@ -657,6 +679,16 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { EMSESP_Status.test_mode = false; // default value } + // shower_timer + if (!(EMSESP_Status.shower_timer = json["shower_timer"])) { + EMSESP_Status.shower_timer = false; // default value + } + + // shower_alert + if (!(EMSESP_Status.shower_alert = json["shower_alert"])) { + EMSESP_Status.shower_alert = false; // default value + } + return false; // always save the settings } @@ -668,6 +700,8 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { json["thermostat_type"] = EMS_Thermostat.type_id; json["boiler_type"] = EMS_Boiler.type_id; json["test_mode"] = EMSESP_Status.test_mode; + json["shower_timer"] = EMSESP_Status.shower_timer; + json["shower_alert"] = EMSESP_Status.shower_alert; return true; } @@ -691,7 +725,8 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c EMSESP_Status.led_enabled = false; ok = true; // let's make sure LED is really off - digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off + digitalWrite(EMSESP_Status.led_gpio, + (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off } } @@ -724,12 +759,12 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c // dallas_parasite if ((strcmp(setting, "dallas_parasite") == 0) && (wc == 2)) { - if (strcmp(value, "true") == 0) { + if (strcmp(value, "on") == 0) { EMSESP_Status.dallas_parasite = true; - ok = true; - } else if (strcmp(value, "false") == 0) { + ok = true; + } else if (strcmp(value, "off") == 0) { EMSESP_Status.dallas_parasite = false; - ok = true; + ok = true; } } @@ -744,10 +779,31 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c EMS_Boiler.type_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); ok = true; } + + // shower timer + if ((strcmp(setting, "shower_timer") == 0) && (wc == 2)) { + if (strcmp(value, "on") == 0) { + EMSESP_Status.shower_timer = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + EMSESP_Status.shower_timer = false; + ok = true; + } + } + + // shower alert + if ((strcmp(setting, "shower_alert") == 0) && (wc == 2)) { + if (strcmp(value, "on") == 0) { + EMSESP_Status.shower_alert = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + EMSESP_Status.shower_alert = false; + ok = true; + } + } } if (action == MYESP_FSACTION_LIST) { - myDebug(" test_mode=%s", EMSESP_Status.test_mode ? "on" : "off"); myDebug(" led=%s", EMSESP_Status.led_enabled ? "on" : "off"); myDebug(" led_gpio=%d", EMSESP_Status.led_gpio); myDebug(" dallas_gpio=%d", EMSESP_Status.dallas_gpio); @@ -766,6 +822,10 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } else { myDebug(" boiler_type=%02X", EMS_Boiler.type_id); } + + myDebug(" test_mode=%s", EMSESP_Status.test_mode ? "on" : "off"); + myDebug(" shower_timer=%s", EMSESP_Status.shower_timer ? "on" : "off"); + myDebug(" shower_alert=%s", EMSESP_Status.shower_alert ? "on" : "off"); } return ok; @@ -1015,7 +1075,7 @@ void WIFICallback() { // This is done after we have a WiFi signal to avoid any resource conflicts if (myESP.getUseSerial()) { - myDebug("Warning! EMS bus disabled when in Serial mode. Use 'set serial off' to enable."); + myDebug("Warning! EMS bus disabled when in Serial mode. Use 'set serial off' to start EMS."); } else { emsuart_init(); myDebug("[UART] Opened Rx/Tx connection"); @@ -1027,8 +1087,8 @@ void WIFICallback() { // Initialize the boiler settings and shower settings void initEMSESP() { // general settings - EMSESP_Status.shower_timer = BOILER_SHOWER_TIMER; - EMSESP_Status.shower_alert = BOILER_SHOWER_ALERT; + EMSESP_Status.shower_timer = false; + EMSESP_Status.shower_alert = false; EMSESP_Status.led_enabled = true; // LED is on by default EMSESP_Status.test_mode = false; @@ -1230,9 +1290,9 @@ void setup() { // enable regular checks if not in test mode if (!EMSESP_Status.test_mode) { - publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post MQTT values - regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS - publishSensorValuesTimer.attach(PUBLISHSENSORVALUES_TIME, do_publishSensorValues); // post MQTT sensor values + publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post MQTT values + regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS + publishSensorValuesTimer.attach(PUBLISHSENSORVALUES_TIME, do_publishSensorValues); // post MQTT sensor values } // set pin for LED @@ -1244,7 +1304,6 @@ void setup() { // check for Dallas sensors EMSESP_Status.dallas_sensors = ds18.setup(EMSESP_Status.dallas_gpio, EMSESP_Status.dallas_parasite); // returns #sensors - } // diff --git a/src/ems.cpp b/src/ems.cpp index 8d7d93f74..7e68e3d51 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -892,7 +892,7 @@ void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.wWSelTemp = data[2]; EMS_Boiler.wWCircPump = (data[6] == 0xFF); // 0xFF means on EMS_Boiler.wWDesiredTemp = data[8]; - EMS_Boiler.wWComfort = (data[EMS_OFFSET_UBAParameterWW_wwComfort] == 0x00); + EMS_Boiler.wWComfort = data[EMS_OFFSET_UBAParameterWW_wwComfort]; EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish } @@ -1098,7 +1098,6 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { return; } - bool do_save = false; uint8_t product_id = data[0]; char version[10] = {0}; snprintf(version, sizeof(version), "%02d.%02d", data[1], data[2]); @@ -1135,7 +1134,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.product_id = Boiler_Types[i].product_id; strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); - do_save = true; + myESP.fs_saveConfig(); // save config to SPIFFS ems_getBoilerValues(); // get Boiler values that we would usually have to wait for } @@ -1178,7 +1177,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { EMS_Thermostat.product_id = product_id; strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); - do_save = true; + myESP.fs_saveConfig(); // save config to SPIFFS // get Thermostat values (if supported) ems_getThermostatValues(); @@ -1187,10 +1186,6 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", type, product_id, version); } - // if the boiler or thermostat values have changed, save them to SPIFFS - if (do_save) { - myESP.fs_saveConfig(); - } } /* @@ -1737,22 +1732,32 @@ void ems_setWarmWaterTemp(uint8_t temperature) { /** * Set the warm water mode to comfort to Eco/Comfort + * 1 = Hot, 2 = Eco, 3 = Intelligent */ -void ems_setWarmWaterModeComfort(bool comfort) { - myDebug("Setting boiler warm water to comfort mode %s\n", comfort ? "Comfort" : "Eco"); - +void ems_setWarmWaterModeComfort(uint8_t comfort) { _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter + if (comfort == 1) { + myDebug("Setting boiler warm water comfort mode to Comfort"); + EMS_TxTelegram.dataValue = EMS_VALUE_UBAParameterWW_wwComfort_Hot; + } else if (comfort == 2) { + myDebug("Setting boiler warm water comfort mode to Eco"); + EMS_TxTelegram.dataValue = EMS_VALUE_UBAParameterWW_wwComfort_Eco; + } else if (comfort == 3) { + myDebug("Setting boiler warm water comfort mode to Intelligent"); + EMS_TxTelegram.dataValue = EMS_VALUE_UBAParameterWW_wwComfort_Intelligent; + } else { + return; // invalid comfort value + } + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; EMS_TxTelegram.dest = EMS_Boiler.type_id; EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwComfort; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate - EMS_TxTelegram.dataValue = - (comfort ? EMS_VALUE_UBAParameterWW_wwComfort_Comfort : EMS_VALUE_UBAParameterWW_wwComfort_Eco); // 0x00 is on, 0xD8 is off EMS_TxQueue.push(EMS_TxTelegram); } diff --git a/src/ems.h b/src/ems.h index 91e8c3070..e5e4af523 100644 --- a/src/ems.h +++ b/src/ems.h @@ -254,7 +254,7 @@ void ems_setPoll(bool b); void ems_setTxEnabled(bool b); void ems_setLogging(_EMS_SYS_LOGGING loglevel); void ems_setEmsRefreshed(bool b); -void ems_setWarmWaterModeComfort(bool comfort); +void ems_setWarmWaterModeComfort(uint8_t comfort); bool ems_checkEMSBUSAlive(); void ems_setModels(); diff --git a/src/ems_devices.h b/src/ems_devices.h index bae98b500..9a27f957a 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -32,11 +32,12 @@ #define EMS_TYPE_UBASetPoints 0x1A #define EMS_TYPE_UBAFunctionTest 0x1D -#define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature -#define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated -#define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode -#define EMS_VALUE_UBAParameterWW_wwComfort_Comfort 0x00 // the value for comfort -#define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco +#define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature +#define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated +#define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode +#define EMS_VALUE_UBAParameterWW_wwComfort_Hot 0x00 // the value for hot +#define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco +#define EMS_VALUE_UBAParameterWW_wwComfort_Intelligent 0xEC // the value for intelligent /* * Thermostats... From 5a350c586ac23d2f3ab3508f7859edce1dbcd486 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 16 Mar 2019 12:51:48 +0100 Subject: [PATCH 19/59] minor changes --- lib/MyESP/MyESP.cpp | 277 +++++++++++++++++++++++--------------------- lib/MyESP/MyESP.h | 27 +++-- 2 files changed, 162 insertions(+), 142 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index f578b0751..bc7a931f1 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -12,14 +12,6 @@ EEPROM_Rotate EEPROMr; #endif -#define RTC_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0)) - -/* Days in a month */ -static uint8_t RTC_Months[2][12] = { - {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, /* Not leap year */ - {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} /* Leap year */ -}; - // constructor MyESP::MyESP() { _app_hostname = strdup("MyESP"); @@ -32,6 +24,8 @@ MyESP::MyESP() { _telnetcommand_callback = NULL; _telnet_callback = NULL; + _command[0] = '\0'; + _fs_callback = NULL; _fs_settings_callback = NULL; @@ -156,12 +150,12 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { // finally if we don't want Serial anymore, turn it off if (!_use_serial) { - Serial.println("Disabling serial port"); + Serial.println(FPSTR("Disabling serial port")); Serial.flush(); Serial.end(); SerialAndTelnet.setSerial(NULL); } else { - Serial.println("Using serial port output"); + Serial.println(FPSTR("Using serial port output")); } // call any final custom settings @@ -180,6 +174,11 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str()); myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str()); + // we could be in panic mode so enable Serial again + SerialAndTelnet.setSerial(&Serial); + Serial.println(FPSTR("Enabling serial port output")); + _use_serial = true; + // call any final custom settings if (_wifi_callback) { _wifi_callback(); @@ -304,7 +303,7 @@ void MyESP::_mqtt_setup() { // WiFI setup void MyESP::_wifi_setup() { - jw.setHostname(_app_hostname); // Set WIFI hostname (otherwise it would be ESP-XXXXXX) + jw.setHostname(_app_hostname); // Set WIFI hostname jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); jw.enableAP(false); jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); @@ -315,7 +314,7 @@ void MyESP::_wifi_setup() { jw.cleanNetworks(); // Clean existing network configuration jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network - WiFi.setSleepMode(WIFI_NONE_SLEEP); // TODO: possible fix for wifi dropouts in core 2.5.0 + WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 } // set the callback function for the OTA onstart @@ -398,7 +397,6 @@ void MyESP::setBoottime(const char * boottime) { void MyESP::_eeprom_setup() { #ifdef CRASH EEPROMr.size(4); - //EEPROMr.offset(EEPROM_ROTATE_DATA); EEPROMr.begin(SPI_FLASH_SEC_SIZE); #endif } @@ -438,91 +436,65 @@ void MyESP::_telnet_setup() { memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); } -// https://stackoverflow.com/questions/43063071/the-arduino-ntp-i-want-print-out-datadd-mm-yyyy -void MyESP::_printBuildTime(unsigned long unix) { - // compensate for summer/winter time and CET. Can't be bothered to work out DST. - // add 3600 to the UNIX time during winter, (3600 s = 1 h), and 7200 during summer (DST). - unix += 3600; // add 1 hour - - uint8_t Day, Month; - - uint8_t Seconds = unix % 60; /* Get seconds from unix */ - unix /= 60; /* Go to minutes */ - uint8_t Minutes = unix % 60; /* Get minutes */ - unix /= 60; /* Go to hours */ - uint8_t Hours = unix % 24; /* Get hours */ - unix /= 24; /* Go to days */ - - uint16_t year = 1970; /* Process year */ - while (1) { - if (RTC_LEAP_YEAR(year)) { - if (unix >= 366) { - unix -= 366; - } else { - break; - } - } else if (unix >= 365) { - unix -= 365; - } else { - break; - } - year++; - } - - /* Get year in xx format */ - uint8_t Year = (uint8_t)(year - 2000); - /* Get month */ - for (Month = 0; Month < 12; Month++) { - if (RTC_LEAP_YEAR(year)) { - if (unix >= (uint32_t)RTC_Months[1][Month]) { - unix -= RTC_Months[1][Month]; - } else { - break; - } - } else if (unix >= (uint32_t)RTC_Months[0][Month]) { - unix -= RTC_Months[0][Month]; - } else { - break; - } - } - - Month++; /* Month starts with 1 */ - Day = unix + 1; /* Date starts with 1 */ - - SerialAndTelnet.printf("%02d:%02d:%02d %d/%d/%d", Hours, Minutes, Seconds, Day, Month, Year); -} - // Show help of commands void MyESP::_consoleShowHelp() { SerialAndTelnet.println(); - SerialAndTelnet.printf("* Connected to: %s version %s", _app_name, _app_version); + SerialAndTelnet.printf(PSTR("* Connected to: %s version %s"), _app_name, _app_version); SerialAndTelnet.println(); if (WiFi.getMode() & WIFI_AP) { - SerialAndTelnet.printf("* Device is in AP mode with SSID %s", jw.getAPSSID().c_str()); + SerialAndTelnet.printf(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); } else { - SerialAndTelnet.printf("* Hostname: %s (%s)", getESPhostname().c_str(), WiFi.localIP().toString().c_str()); + SerialAndTelnet.printf(PSTR("* Hostname: %s (%s)"), getESPhostname().c_str(), WiFi.localIP().toString().c_str()); SerialAndTelnet.println(); - SerialAndTelnet.printf("* WiFi SSID: %s (signal %d%%)", WiFi.SSID().c_str(), getWifiQuality()); + SerialAndTelnet.printf(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality()); SerialAndTelnet.println(); - SerialAndTelnet.printf("* MQTT is %s", mqttClient.connected() ? "connected" : "disconnected"); + SerialAndTelnet.printf(PSTR("* MQTT is %s"), mqttClient.connected() ? "connected" : "disconnected"); } SerialAndTelnet.println(); - - if (_boottime != NULL) { - SerialAndTelnet.printf("* Boot time: %s", _boottime); - SerialAndTelnet.println(); - } - SerialAndTelnet.println(FPSTR("*")); SerialAndTelnet.println(FPSTR("* Commands:")); SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit telnet")); - SerialAndTelnet.println(FPSTR("* set | reboot | system")); + SerialAndTelnet.println(FPSTR("* set, system, reboot")); +#ifdef CRASH SerialAndTelnet.println(FPSTR("* crash ")); +#endif + + // print custom commands if available. Taken from progmem + if (_telnetcommand_callback) { + // find the longest key length so we can right align it + uint8_t max_len = 0; + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if ((strlen(_helpProjectCmds[i].key) > max_len) && (!_helpProjectCmds[i].set)) { + max_len = strlen(_helpProjectCmds[i].key); + } + } + + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if (!_helpProjectCmds[i].set) { + SerialAndTelnet.print(FPSTR("* ")); + SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length + SerialAndTelnet.print(FPSTR(" ")); // padding + } + SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); + } + } + } + + SerialAndTelnet.println(); // newline +} + + +// print all set commands and current values +void MyESP::_printSetCommands() { + SerialAndTelnet.println(); // newline + SerialAndTelnet.println(FPSTR("The following set commands are available:")); + SerialAndTelnet.println(); + SerialAndTelnet.println(FPSTR("* set erase")); SerialAndTelnet.println(FPSTR("* set wifi [ssid] [password]")); SerialAndTelnet.println(FPSTR("* set [value]")); - SerialAndTelnet.println(FPSTR("* set erase")); SerialAndTelnet.println(FPSTR("* set serial")); // print custom commands if available. Taken from progmem @@ -530,21 +502,55 @@ void MyESP::_consoleShowHelp() { // find the longest key length so we can right align it uint8_t max_len = 0; for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { - if (strlen(_helpProjectCmds[i].key) > max_len) + if ((strlen(_helpProjectCmds[i].key) > max_len) && (_helpProjectCmds[i].set)) { max_len = strlen(_helpProjectCmds[i].key); + } } for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { - SerialAndTelnet.print(FPSTR("* ")); - SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); - for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length - SerialAndTelnet.print(FPSTR(" ")); // padding + if (_helpProjectCmds[i].set) { + SerialAndTelnet.print(FPSTR("* set ")); + SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length + SerialAndTelnet.print(FPSTR(" ")); // padding + } + SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); } - SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); } } SerialAndTelnet.println(); // newline + SerialAndTelnet.println(FPSTR("Stored settings:")); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" wifi=%s ", (!_wifi_ssid) ? "" : _wifi_ssid); + if (!_wifi_password) { + SerialAndTelnet.print(FPSTR("")); + } else { + for (uint8_t i = 0; i < strlen(_wifi_password); i++) + SerialAndTelnet.print(FPSTR("*")); + } + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_host=%s", (!_mqtt_host) ? "" : _mqtt_host); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_username=%s", (!_mqtt_username) ? "" : _mqtt_username); + SerialAndTelnet.println(); + SerialAndTelnet.print(FPSTR(" mqtt_password=")); + if (!_mqtt_password) { + SerialAndTelnet.print(FPSTR("")); + } else { + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) + SerialAndTelnet.print(FPSTR("*")); + } + + SerialAndTelnet.println(); + SerialAndTelnet.printf(" serial=%s", (_use_serial) ? "on" : "off"); + + SerialAndTelnet.println(); + + // print any custom settings + (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); + + SerialAndTelnet.println(); } // reset / restart @@ -638,9 +644,11 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) if (strcmp(value, "on") == 0) { _use_serial = true; ok = true; + SerialAndTelnet.println(FPSTR("Please reboot ESP to activate Serial mode.")); } else if (strcmp(value, "off") == 0) { _use_serial = false; ok = true; + SerialAndTelnet.println(FPSTR("Please reboot ESP to deactivate Serial mode.")); } else { ok = false; } @@ -651,16 +659,16 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) } if (!ok) { - SerialAndTelnet.println("\nInvalid parameter for set command."); + SerialAndTelnet.println(FPSTR("\nInvalid parameter for set command.")); return; } - + // check for 2 params if (value == nullptr) { - SerialAndTelnet.printf("%s setting reset to its default value.", setting); + SerialAndTelnet.printf(PSTR("%s setting reset to its default value."), setting); } else { // must be 3 params - SerialAndTelnet.printf("%s changed.", setting); + SerialAndTelnet.printf(PSTR("%s changed."), setting); } SerialAndTelnet.println(); @@ -689,38 +697,7 @@ void MyESP::_telnetCommand(char * commandLine) { // set command if (strcmp(ptrToCommandName, "set") == 0) { if (wc == 1) { - SerialAndTelnet.println(); - SerialAndTelnet.println("Stored settings:"); - SerialAndTelnet.printf(" wifi=%s ", (!_wifi_ssid) ? "" : _wifi_ssid); - if (!_wifi_password) { - SerialAndTelnet.print(""); - } else { - for (uint8_t i = 0; i < strlen(_wifi_password); i++) - SerialAndTelnet.print("*"); - } - SerialAndTelnet.println(); - SerialAndTelnet.printf(" mqtt_host=%s", (!_mqtt_host) ? "" : _mqtt_host); - SerialAndTelnet.println(); - SerialAndTelnet.printf(" mqtt_username=%s", (!_mqtt_username) ? "" : _mqtt_username); - SerialAndTelnet.println(); - SerialAndTelnet.printf(" mqtt_password="); - if (!_mqtt_password) { - SerialAndTelnet.print(""); - } else { - for (uint8_t i = 0; i < strlen(_mqtt_password); i++) - SerialAndTelnet.print("*"); - } - - SerialAndTelnet.println(); - SerialAndTelnet.printf(" serial=%s", (_use_serial) ? "on" : "off"); - - SerialAndTelnet.println(); - - // print custom settings - (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); - - SerialAndTelnet.println(); - SerialAndTelnet.println("Usage: set [value...]"); + _printSetCommands(); } else if (wc == 2) { char * setting = _telnet_readWord(); _changeSetting(1, setting, NULL); @@ -780,25 +757,65 @@ String MyESP::getESPhostname() { return (hostname); } +// returns build time as a String - copied for espurna. see (c) +String MyESP::_buildTime() { + const char time_now[] = __TIME__; // hh:mm:ss + unsigned int hour = atoi(&time_now[0]); + unsigned int minute = atoi(&time_now[3]); + unsigned int second = atoi(&time_now[6]); + + const char date_now[] = __DATE__; // Mmm dd yyyy + const char * months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + unsigned int month = 0; + for (int i = 0; i < 12; i++) { + if (strncmp(date_now, months[i], 3) == 0) { + month = i + 1; + break; + } + } + unsigned int day = atoi(&date_now[3]); + unsigned int year = atoi(&date_now[7]); + + char buffer[20]; + snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hour, minute, second); + + return String(buffer); +} + +// returns system uptime - copied for espurna. see (c) +unsigned long MyESP::_getUptime() { + static unsigned long last_uptime = 0; + static unsigned char uptime_overflows = 0; + + if (millis() < last_uptime) + ++uptime_overflows; + last_uptime = millis(); + unsigned long uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); + + return uptime_seconds; +} + // print out ESP system stats // for battery power is ESP.getVcc() void MyESP::showSystemStats() { -#ifdef BUILD_TIME - myDebug_P("[SYSTEM] Build timestamp: %s)", _printBuildTime(BUILD_TIME);); -#endif - + myDebug_P(PSTR("[APP] %s version: %s"), _app_name, _app_version); myDebug_P(PSTR("[APP] MyESP version: %s"), MYESP_VERSION); + myDebug_P(PSTR("[APP] Build timestamp: %s"), _buildTime().c_str()); + if (_boottime != NULL) { + myDebug_P(PSTR("[APP] Boot time: %s"), _boottime); + } + myDebug_P(PSTR("[APP] Uptime: %d seconds"), _getUptime()); myDebug_P(PSTR("[APP] System Load: %d%%"), getSystemLoadAverage()); if (WiFi.getMode() & WIFI_AP) { myDebug_P(PSTR("[WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); } else { - myDebug_P(PSTR("[WIFI] Wifi Hostname: %s"), getESPhostname().c_str()); - myDebug_P(PSTR("[WIFI] Wifi IP: %s"), WiFi.localIP().toString().c_str()); - myDebug_P(PSTR("[WIFI] Wifi signal strength: %d%%"), getWifiQuality()); + myDebug_P(PSTR("[WIFI] WiFi Hostname: %s"), getESPhostname().c_str()); + myDebug_P(PSTR("[WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] WiFi signal strength: %d%%"), getWifiQuality()); } - myDebug_P(PSTR("[WIFI] Wifi MAC: %s"), WiFi.macAddress().c_str()); + myDebug_P(PSTR("[WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); #ifdef CRASH char output_str[80] = {0}; diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 2c4b2078a..646c0874c 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6d" +#define MYESP_VERSION "1.1.6" #include #include @@ -78,10 +78,9 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); #define COLOR_BOLD_OFF "\x1B[22m" // fix by Scott Arlott to support Linux // SPIFFS -#define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v6/assistant/ +#define SPIFFS_MAXSIZE 600 // https://arduinojson.org/v6/assistant/ // CRASH -#define EEPROM_ROTATE_DATA 11 // Reserved for the EEPROM_ROTATE library (3 bytes) /** * Structure of the single crash data set * @@ -111,8 +110,8 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); #define SAVE_CRASH_STACK_END 0x1E // 4 bytes #define SAVE_CRASH_STACK_TRACE 0x22 // variable - typedef struct { + bool set; // is it a set command char key[40]; char description[100]; } command_t; @@ -133,6 +132,8 @@ constexpr size_t ArraySize(T (&)[N]) { return N; } +#define UPTIME_OVERFLOW 4294967295 // Uptime overflow value + // class definition class MyESP { public: @@ -190,7 +191,6 @@ class MyESP { int getWifiQuality(); void showSystemStats(); - private: // mqtt AsyncMqttClient mqttClient; @@ -257,17 +257,20 @@ class MyESP { void _fs_printConfig(); void _fs_eraseConfig(); + // settings fs_callback_f _fs_callback; fs_settings_callback_f _fs_settings_callback; + void _printSetCommands(); // general - char * _app_hostname; - char * _app_name; - char * _app_version; - char * _boottime; - bool _suspendOutput; - bool _use_serial; - void _printBuildTime(unsigned long rawTime); + char * _app_hostname; + char * _app_name; + char * _app_version; + char * _boottime; + bool _suspendOutput; + bool _use_serial; + unsigned long _getUptime(); + String _buildTime(); // load average (0..100) void _calculateLoad(); From f22268d843b9a13eb636a6870f44f606f193fd0e Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 16 Mar 2019 12:51:57 +0100 Subject: [PATCH 20/59] 1.6.0 --- CHANGELOG.md | 6 ++++-- src/version.h | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebf296a4..6f933f965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,17 +12,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - system command to show ESP stats - crash command to see stack of last system crash, with .py files to track stack dump - publish dallas external temp sensors to MQTT (Thanks @JewelZB) +- shower timer and shower alert options available via set commands +- Added support for warm water modes Hot, Comfort and Intelligent (https://github.com/proddy/EMS-ESP/issues/67) ### Fixed - incorrect rendering of null temperature values (the -3200 degrees issue) - OTA is more stable - Added a hack to overcome WiFi power issues in esp core 2.5.0 libraries causing constant re-connects +- Performance issues with telnet output ### Changed -- included various fixes and suggestions from nomis (Simon Arlott) - +- included various fixes and suggestions from @nomis - upgraded MyESP library ## [1.5.6] 2019-03-09 diff --git a/src/version.h b/src/version.h index 5b7e1213c..5cf1a5fef 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.5.7c" +#define APP_VERSION "1.6.0b" #define APP_HOSTNAME "ems-esp" From 3f3a475ee15b2e7ccedf67564a8f64b9982923e2 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 17 Mar 2019 14:59:07 +0100 Subject: [PATCH 21/59] 1.6.0 b2 --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index 5cf1a5fef..ff30e1dab 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.6.0b" +#define APP_VERSION "1.6.0b2" #define APP_HOSTNAME "ems-esp" From 4a559866c7a56435714fa396a3e866860c14eb2b Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 17 Mar 2019 14:59:37 +0100 Subject: [PATCH 22/59] 1.6.0 b2 --- lib/MyESP/MyESP.cpp | 194 +++++++++++++++++++++++------------------ lib/MyESP/MyESP.h | 12 +-- platformio.ini-example | 4 +- src/ems-esp.ino | 46 ++++++++-- src/emsuart.cpp | 19 ++-- src/emsuart.h | 1 + src/my_config.h | 3 +- 7 files changed, 173 insertions(+), 106 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index bc7a931f1..090d0aa2b 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -53,6 +53,9 @@ MyESP::MyESP() { _wifi_callback = NULL; _wifi_connected = false; + _ota_pre_callback = NULL; + _ota_post_callback = NULL; + _suspendOutput = false; } @@ -86,7 +89,6 @@ void MyESP::myDebug(const char * format, ...) { delete[] buffer; } - // for flashmemory. Must use PSTR() void MyESP::myDebug_P(PGM_P format_P, ...) { if (_suspendOutput) @@ -105,10 +107,12 @@ void MyESP::myDebug_P(PGM_P format_P, ...) { va_end(args); +#ifdef MYESP_TIMESTAMP // capture & print timestamp char timestamp[10] = {0}; snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); SerialAndTelnet.print(timestamp); +#endif SerialAndTelnet.println(buffer); @@ -150,12 +154,12 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { // finally if we don't want Serial anymore, turn it off if (!_use_serial) { - Serial.println(FPSTR("Disabling serial port")); + myDebug_P(PSTR("Disabling serial port")); Serial.flush(); Serial.end(); SerialAndTelnet.setSerial(NULL); } else { - Serial.println(FPSTR("Using serial port output")); + myDebug_P(PSTR("Using serial port output")); } // call any final custom settings @@ -175,9 +179,12 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str()); // we could be in panic mode so enable Serial again - SerialAndTelnet.setSerial(&Serial); - Serial.println(FPSTR("Enabling serial port output")); - _use_serial = true; + if (!_use_serial) { + SerialAndTelnet.setSerial(&Serial); + _use_serial = true; + } + + myDebug_P(PSTR("Enabling serial port output")); // call any final custom settings if (_wifi_callback) { @@ -201,6 +208,12 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { } } +// return true if in WiFi AP mode +// does not work after wifi reset on ESP32 yet. See https://github.com/espressif/arduino-esp32/issues/1306 +bool MyESP::isAPmode() { + return (WiFi.getMode() & WIFI_AP); +} + // received MQTT message // we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { @@ -314,12 +327,15 @@ void MyESP::_wifi_setup() { jw.cleanNetworks(); // Clean existing network configuration jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network +#if defined(ESP8266) WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 +#endif } // set the callback function for the OTA onstart -void MyESP::setOTA(ota_callback_f OTACallback) { - _ota_callback = OTACallback; +void MyESP::setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post) { + _ota_pre_callback = OTACallback_pre; + _ota_post_callback = OTACallback_post; } // OTA callback when the upload process starts @@ -341,8 +357,8 @@ void MyESP::_OTACallback() { EEPROMr.commit(); #endif - if (_ota_callback) { - (_ota_callback)(); // call custom function to handle mqtt receives + if (_ota_pre_callback) { + (_ota_pre_callback)(); // call custom function } } @@ -438,27 +454,23 @@ void MyESP::_telnet_setup() { // Show help of commands void MyESP::_consoleShowHelp() { - SerialAndTelnet.println(); - SerialAndTelnet.printf(PSTR("* Connected to: %s version %s"), _app_name, _app_version); - SerialAndTelnet.println(); + myDebug_P(PSTR("")); + myDebug_P(PSTR("* Connected to: %s version %s"), _app_name, _app_version); - if (WiFi.getMode() & WIFI_AP) { - SerialAndTelnet.printf(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + if (isAPmode()) { + myDebug_P(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); } else { - SerialAndTelnet.printf(PSTR("* Hostname: %s (%s)"), getESPhostname().c_str(), WiFi.localIP().toString().c_str()); - SerialAndTelnet.println(); - SerialAndTelnet.printf(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality()); - SerialAndTelnet.println(); - SerialAndTelnet.printf(PSTR("* MQTT is %s"), mqttClient.connected() ? "connected" : "disconnected"); + myDebug_P(PSTR("* Hostname: %s (%s)"), _getESPhostname().c_str(), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality()); + myDebug_P(PSTR("* MQTT is %s"), mqttClient.connected() ? "connected" : "disconnected"); } - SerialAndTelnet.println(); - SerialAndTelnet.println(FPSTR("*")); - SerialAndTelnet.println(FPSTR("* Commands:")); - SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit telnet")); - SerialAndTelnet.println(FPSTR("* set, system, reboot")); + myDebug_P(PSTR("*")); + myDebug_P(PSTR("* Commands:")); + myDebug_P(PSTR("* ?=help, CTRL-D=quit telnet")); + myDebug_P(PSTR("* set, system, reboot")); #ifdef CRASH - SerialAndTelnet.println(FPSTR("* crash ")); + myDebug_P(PSTR("* crash ")); #endif // print custom commands if available. Taken from progmem @@ -482,20 +494,18 @@ void MyESP::_consoleShowHelp() { } } } - - SerialAndTelnet.println(); // newline + myDebug_P(PSTR("")); // newline } - // print all set commands and current values void MyESP::_printSetCommands() { - SerialAndTelnet.println(); // newline - SerialAndTelnet.println(FPSTR("The following set commands are available:")); - SerialAndTelnet.println(); - SerialAndTelnet.println(FPSTR("* set erase")); - SerialAndTelnet.println(FPSTR("* set wifi [ssid] [password]")); - SerialAndTelnet.println(FPSTR("* set [value]")); - SerialAndTelnet.println(FPSTR("* set serial")); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("The following set commands are available:")); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("* set erase")); + myDebug_P(PSTR("* set wifi [ssid] [password]")); + myDebug_P(PSTR("* set [value]")); + myDebug_P(PSTR("* set serial")); // print custom commands if available. Taken from progmem if (_telnetcommand_callback) { @@ -519,21 +529,19 @@ void MyESP::_printSetCommands() { } } - SerialAndTelnet.println(); // newline - SerialAndTelnet.println(FPSTR("Stored settings:")); - SerialAndTelnet.println(); - SerialAndTelnet.printf(" wifi=%s ", (!_wifi_ssid) ? "" : _wifi_ssid); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("Stored settings:")); + myDebug_P(PSTR("")); // newline + SerialAndTelnet.printf(PSTR(" wifi=%s "), (!_wifi_ssid) ? "" : _wifi_ssid); if (!_wifi_password) { SerialAndTelnet.print(FPSTR("")); } else { for (uint8_t i = 0; i < strlen(_wifi_password); i++) SerialAndTelnet.print(FPSTR("*")); } - SerialAndTelnet.println(); - SerialAndTelnet.printf(" mqtt_host=%s", (!_mqtt_host) ? "" : _mqtt_host); - SerialAndTelnet.println(); - SerialAndTelnet.printf(" mqtt_username=%s", (!_mqtt_username) ? "" : _mqtt_username); - SerialAndTelnet.println(); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" mqtt_host=%s"), (!_mqtt_host) ? "" : _mqtt_host); + myDebug_P(PSTR(" mqtt_username=%s"), (!_mqtt_username) ? "" : _mqtt_username); SerialAndTelnet.print(FPSTR(" mqtt_password=")); if (!_mqtt_password) { SerialAndTelnet.print(FPSTR("")); @@ -542,15 +550,13 @@ void MyESP::_printSetCommands() { SerialAndTelnet.print(FPSTR("*")); } - SerialAndTelnet.println(); - SerialAndTelnet.printf(" serial=%s", (_use_serial) ? "on" : "off"); - - SerialAndTelnet.println(); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" serial=%s"), (_use_serial) ? "on" : "off"); // print any custom settings (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); - SerialAndTelnet.println(); + myDebug_P(PSTR("")); // newline } // reset / restart @@ -588,10 +594,10 @@ void MyESP::_changeSetting2(const char * setting, const char * value1, const cha } (void)fs_saveConfig(); - SerialAndTelnet.println("WiFi settings changed. Reconnecting..."); - jw.disconnect(); - jw.cleanNetworks(); - jw.addNetwork(_wifi_ssid, _wifi_password); + myDebug_P(PSTR("WiFi settings changed. Reboot ESP.")); + //jw.disconnect(); + //jw.cleanNetworks(); + //jw.addNetwork(_wifi_ssid, _wifi_password); } } @@ -644,11 +650,11 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) if (strcmp(value, "on") == 0) { _use_serial = true; ok = true; - SerialAndTelnet.println(FPSTR("Please reboot ESP to activate Serial mode.")); + myDebug_P(PSTR("Reboot ESP to activate Serial mode.")); } else if (strcmp(value, "off") == 0) { _use_serial = false; ok = true; - SerialAndTelnet.println(FPSTR("Please reboot ESP to deactivate Serial mode.")); + myDebug_P(PSTR("Reboot ESP to deactivate Serial mode.")); } else { ok = false; } @@ -659,18 +665,19 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) } if (!ok) { - SerialAndTelnet.println(FPSTR("\nInvalid parameter for set command.")); + myDebug_P(PSTR("\nInvalid parameter for set command.")); return; } - + // check for 2 params if (value == nullptr) { - SerialAndTelnet.printf(PSTR("%s setting reset to its default value."), setting); + myDebug_P(PSTR("%s setting reset to its default value."), setting); } else { // must be 3 params - SerialAndTelnet.printf(PSTR("%s changed."), setting); + myDebug_P(PSTR("%s changed."), setting); } - SerialAndTelnet.println(); + + myDebug_P(PSTR("")); // newline (void)fs_saveConfig(); } @@ -725,8 +732,8 @@ void MyESP::_telnetCommand(char * commandLine) { return; } - - // crash command +// crash command +#ifdef CRASH if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { char * cmd = _telnet_readWord(); if (strcmp(cmd, "dump") == 0) { @@ -739,13 +746,14 @@ void MyESP::_telnetCommand(char * commandLine) { } return; // don't call custom command line callback } +#endif // call callback function (_telnetcommand_callback)(wc, commandLine); } // returns WiFi hostname as a String object -String MyESP::getESPhostname() { +String MyESP::_getESPhostname() { String hostname; #if defined(ARDUINO_ARCH_ESP32) @@ -807,10 +815,10 @@ void MyESP::showSystemStats() { myDebug_P(PSTR("[APP] Uptime: %d seconds"), _getUptime()); myDebug_P(PSTR("[APP] System Load: %d%%"), getSystemLoadAverage()); - if (WiFi.getMode() & WIFI_AP) { + if (isAPmode()) { myDebug_P(PSTR("[WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); } else { - myDebug_P(PSTR("[WIFI] WiFi Hostname: %s"), getESPhostname().c_str()); + myDebug_P(PSTR("[WIFI] WiFi Hostname: %s"), _getESPhostname().c_str()); myDebug_P(PSTR("[WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); myDebug_P(PSTR("[WIFI] WiFi signal strength: %d%%"), getWifiQuality()); } @@ -836,30 +844,38 @@ void MyESP::showSystemStats() { myDebug_P(PSTR("[SYSTEM] Board: %s"), ARDUINO_BOARD); #endif - myDebug_P(PSTR("[SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); myDebug_P(PSTR("[SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); myDebug_P(PSTR("[SYSTEM] SDK version: %s"), ESP.getSdkVersion()); + +#if defined(ESP8266) + myDebug_P(PSTR("[SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); myDebug_P(PSTR("[SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); myDebug_P(PSTR("[SYSTEM] Boot version: %d"), ESP.getBootVersion()); myDebug_P(PSTR("[SYSTEM] Boot mode: %d"), ESP.getBootMode()); //myDebug_P(PSTR("[SYSTEM] Firmware MD5: %s"), (char *)ESP.getSketchMD5().c_str()); +#endif FlashMode_t mode = ESP.getFlashChipMode(); +#if defined(ESP8266) myDebug_P(PSTR("[FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); +#endif myDebug_P(PSTR("[FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); myDebug_P(PSTR("[FLASH] Flash mode: %s"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); +#if defined(ESP8266) myDebug_P(PSTR("[FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); +#endif myDebug_P(PSTR("[FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); myDebug_P(PSTR("[FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); myDebug_P(PSTR("[MEM] Firmware size: %d"), ESP.getSketchSize()); myDebug_P(PSTR("[MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); myDebug_P(PSTR("[MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); myDebug_P(PSTR("[MEM] Free Heap: %d"), ESP.getFreeHeap()); +#if defined(ESP8266) myDebug_P(PSTR("[MEM] Stack: %d"), ESP.getFreeContStack()); +#endif } - // handler for Telnet void MyESP::_telnetHandle() { SerialAndTelnet.handle(); @@ -869,7 +885,7 @@ void MyESP::_telnetHandle() { while (SerialAndTelnet.available()) { char c = SerialAndTelnet.read(); - SerialAndTelnet.serialPrint(c); // echo to Serial if connected + SerialAndTelnet.serialPrint(c); // echo to Serial (if connected) switch (c) { case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF @@ -887,7 +903,6 @@ void MyESP::_telnetHandle() { case '\b': // (^H) handle backspace in input: put a space in last char - coded by Simon Arlott case 0x7F: // (^?) - if (charsRead > 0) { _command[--charsRead] = '\0'; @@ -1070,7 +1085,6 @@ char * MyESP::_mqttTopic(const char * topic) { return _mqtt_topic; } - // print contents of file // assumes Serial is open void MyESP::_fs_printConfig() { @@ -1078,14 +1092,14 @@ void MyESP::_fs_printConfig() { File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); if (!configFile) { - Serial.println(F("[FS] Failed to read file for printing")); + myDebug_P(PSTR("[FS] Failed to read file for printing")); return; } while (configFile.available()) { SerialAndTelnet.print((char)configFile.read()); } - SerialAndTelnet.println(); + myDebug_P(PSTR("")); // newline configFile.close(); } @@ -1133,6 +1147,7 @@ bool MyESP::_fs_loadConfig() { const char * value; + // fetch the standard system parameters value = json["wifi_ssid"]; _wifi_ssid = (value) ? strdup(value) : NULL; @@ -1161,6 +1176,12 @@ bool MyESP::_fs_loadConfig() { // save settings to spiffs bool MyESP::fs_saveConfig() { + bool ok = true; + + if (_ota_pre_callback) { + (_ota_pre_callback)(); + } + StaticJsonDocument doc; JsonObject json = doc.to(); @@ -1183,18 +1204,29 @@ bool MyESP::fs_saveConfig() { File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); if (!configFile) { - Serial.println("[FS] Failed to open config file for writing"); - return false; + myDebug_P(PSTR("[FS] Failed to open config file for writing")); + ok = false; } + /* + if (ok) { + myDebug_P(PSTR("[FS] Writing config file")); + } + */ + // Serialize JSON to file if (serializeJson(json, configFile) == 0) { - Serial.println(F("[FS] Failed to write to file")); + myDebug_P(PSTR("[FS] Failed to write to file")); + ok = false; } configFile.close(); - return true; + if (_ota_post_callback) { + (_ota_post_callback)(); + } + + return ok; } // init the SPIFF file system and load the config @@ -1202,14 +1234,14 @@ bool MyESP::fs_saveConfig() { // force Serial for debugging, and turn it off afterwards void MyESP::_fs_setup() { if (!SPIFFS.begin()) { - Serial.println("[FS] Failed to mount the file system"); + myDebug_P(PSTR("[FS] Failed to mount the file system. Erasing...")); _fs_eraseConfig(); // fix for ESP32 return; } // load the config file. if it doesn't exist (function returns false) create it if (!_fs_loadConfig()) { - // Serial.println("[FS] Re-creating config file"); + myDebug_P(PSTR("[FS] Re-creating config file")); fs_saveConfig(); } @@ -1417,16 +1449,12 @@ void MyESP::crashDump() { } #else void MyESP::crashTest(uint8_t t) { - myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); } void MyESP::crashClear() { - myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); } void MyESP::crashDump() { - myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); } void MyESP::crashInfo() { - myDebug("[CRASH] disabled or not supported. Compile with -DCRASH"); } #endif diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 646c0874c..82a6e8ef2 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6" +#define MYESP_VERSION "1.1.6b1" #include #include @@ -144,6 +144,7 @@ class MyESP { void setWIFICallback(void (*callback)()); void setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); bool isWifiConnected(); + bool isAPmode(); // mqtt bool isMQTTConnected(); @@ -163,7 +164,7 @@ class MyESP { mqtt_callback_f callback); // OTA - void setOTA(ota_callback_f OTACallback); + void setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post); // debug & telnet void myDebug(const char * format, ...); @@ -175,7 +176,7 @@ class MyESP { void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); bool fs_saveConfig(); - // CRASH + // Crash void crashClear(); void crashDump(); void crashTest(uint8_t t); @@ -224,10 +225,11 @@ class MyESP { char * _wifi_ssid; char * _wifi_password; bool _wifi_connected; - String getESPhostname(); + String _getESPhostname(); // ota - ota_callback_f _ota_callback; + ota_callback_f _ota_pre_callback; + ota_callback_f _ota_post_callback; void _ota_setup(); void _OTACallback(); diff --git a/platformio.ini-example b/platformio.ini-example index 8a13925c5..1fde98e32 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -13,10 +13,10 @@ platform = ${common.platform_def} flash_mode = dout ; for production -;build_flags = -Os +build_flags = -Os -w ; for debug -build_flags = -g -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH +; build_flags = -g -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH wifi_settings = ; hard code if you prefer. Recommendation is to set from within the app when in Serial or AP mode diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 7f1bc9b21..8aacf17fe 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -436,16 +436,17 @@ void showInfo() { // send all dallas sensor values as a JSON package to MQTT void publishSensorValues() { StaticJsonDocument doc; - bool hasdata = false; + JsonObject sensors = doc.to(); + + bool hasdata = false; + char label[8] = {0}; + char valuestr[8] = {0}; // for formatting temp // see if the sensor values have changed, if so send - JsonObject sensors = doc.to(); for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { double sensorValue = ds18.getValue(i); if (sensorValue != DS18_DISCONNECTED && sensorValue != DS18_CRC_ERROR) { - char label[8] = {0}; - char valuestr[8] = {0}; // for formatting temp - sprintf(label, "temp_%d", (i + 1)); + sprintf(label, PAYLOAD_EXTERNAL_SENSORS, (i + 1)); sensors[label] = _float_to_char(valuestr, sensorValue); hasdata = true; } @@ -643,53 +644,64 @@ void startThermostatScan(uint8_t start) { // callback for loading/saving settings to the file system (SPIFFS) bool FSCallback(MYESP_FSACTION action, const JsonObject json) { + bool recreate_config = false; + if (action == MYESP_FSACTION_LOAD) { // led if (!(EMSESP_Status.led_enabled = json["led"])) { EMSESP_Status.led_enabled = LED_BUILTIN; // default value + recreate_config = true; } // led_gpio if (!(EMSESP_Status.led_gpio = json["led_gpio"])) { EMSESP_Status.led_gpio = EMSESP_LED_GPIO; // default value + recreate_config = true; } // dallas_gpio if (!(EMSESP_Status.dallas_gpio = json["dallas_gpio"])) { EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; // default value + recreate_config = true; } // dallas_parasite if (!(EMSESP_Status.dallas_parasite = json["dallas_parasite"])) { EMSESP_Status.dallas_parasite = EMSESP_DALLAS_PARASITE; // default value + recreate_config = true; } // thermostat_type if (!(EMS_Thermostat.type_id = json["thermostat_type"])) { EMS_Thermostat.type_id = EMSESP_THERMOSTAT_TYPE; // set default + recreate_config = true; } // boiler_type if (!(EMS_Boiler.type_id = json["boiler_type"])) { EMS_Boiler.type_id = EMSESP_BOILER_TYPE; // set default + recreate_config = true; } // test mode if (!(EMSESP_Status.test_mode = json["test_mode"])) { EMSESP_Status.test_mode = false; // default value + recreate_config = true; } // shower_timer if (!(EMSESP_Status.shower_timer = json["shower_timer"])) { EMSESP_Status.shower_timer = false; // default value + recreate_config = true; } // shower_alert if (!(EMSESP_Status.shower_alert = json["shower_alert"])) { EMSESP_Status.shower_alert = false; // default value + recreate_config = true; } - return false; // always save the settings + return recreate_config; // return false if some settings are missing and we need to rebuild the file } if (action == MYESP_FSACTION_SAVE) { @@ -709,7 +721,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { return false; } -// callback for custom settings when showing Stored Settings +// callback for custom settings when showing Stored Settings with the 'set' command // wc is number of arguments after the 'set' command // returns true if the setting was recognized and changed bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) { @@ -727,6 +739,8 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c // let's make sure LED is really off digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off + } else { + myDebug("Error. Usage: set led "); } } @@ -739,6 +753,8 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } else if (strcmp(value, "off") == 0) { EMSESP_Status.test_mode = false; ok = true; + } else { + myDebug("Error. Usage: set test_mode "); } } @@ -765,6 +781,8 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } else if (strcmp(value, "off") == 0) { EMSESP_Status.dallas_parasite = false; ok = true; + } else { + myDebug("Error. Usage: set dallas_parasite "); } } @@ -788,6 +806,8 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } else if (strcmp(value, "off") == 0) { EMSESP_Status.shower_timer = false; ok = true; + } else { + myDebug("Error. Usage: set shower_timer "); } } @@ -799,6 +819,8 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } else if (strcmp(value, "off") == 0) { EMSESP_Status.shower_alert = false; ok = true; + } else { + myDebug("Error. Usage: set shower_alert "); } } } @@ -969,10 +991,16 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { // OTA callback when the OTA process starts // so we can disable the EMS to avoid any noise -void OTACallback() { +void OTACallback_pre() { emsuart_stop(); } +// OTA callback when the OTA process finishes +// so we can re-enable the UART +void OTACallback_post() { + emsuart_start(); +} + // MQTT Callback to handle incoming/outgoing changes void MQTTCallback(unsigned int type, const char * topic, const char * message) { // we're connected. lets subscribe to some topics @@ -1280,7 +1308,7 @@ void setup() { MQTTCallback); // OTA callback which is called when OTA is starting - myESP.setOTA(OTACallback); + myESP.setOTA(OTACallback_pre, OTACallback_post); // custom settings in SPIFFS myESP.setSettings(FSCallback, SettingsCallback); diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 684900456..7b33507ac 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -129,13 +129,13 @@ void ICACHE_FLASH_ATTR emsuart_init() { system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen); // disable esp debug which will go to Tx and mess up the line - // system_set_os_print(0); // https://github.com/espruino/Espruino/issues/655 - - ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, NULL); - ETS_UART_INTR_ENABLE(); + system_set_os_print(0); // https://github.com/espruino/Espruino/issues/655 // swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively system_uart_swap(); + + ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, NULL); + ETS_UART_INTR_ENABLE(); } /* @@ -143,12 +143,19 @@ void ICACHE_FLASH_ATTR emsuart_init() { */ void ICACHE_FLASH_ATTR emsuart_stop() { ETS_UART_INTR_DISABLE(); - ETS_UART_INTR_ATTACH(NULL, NULL); - system_uart_swap(); // to be sure, swap Tx/Rx back. Idea from Simon Arlott + //ETS_UART_INTR_ATTACH(NULL, NULL); + //system_uart_swap(); // to be sure, swap Tx/Rx back. Idea from Simon Arlott //detachInterrupt(digitalPinToInterrupt(D7)); //noInterrupts(); } +/* + * re-start UART0 driver + */ +void ICACHE_FLASH_ATTR emsuart_start() { + ETS_UART_INTR_ENABLE(); +} + /* * Send a BRK signal * Which is a 11-bit set of zero's (11 cycles) diff --git a/src/emsuart.h b/src/emsuart.h index f5053bda3..e8e371909 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -31,6 +31,7 @@ typedef struct { void ICACHE_FLASH_ATTR emsuart_init(); void ICACHE_FLASH_ATTR emsuart_stop(); +void ICACHE_FLASH_ATTR emsuart_start(); void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len); void ICACHE_FLASH_ATTR emsaurt_tx_poll(); void ICACHE_FLASH_ATTR emsuart_tx_brk(); diff --git a/src/my_config.h b/src/my_config.h index 6248f478d..25d771df3 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -45,7 +45,8 @@ #define TOPIC_SHOWER_COLDSHOT "shower_coldshot" // used to trigger a coldshot from an MQTT command // MQTT for EXTERNAL SENSORS -#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT +#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT +#define PAYLOAD_EXTERNAL_SENSORS "temp_%d" // for formatting the payload for each external dallas sensor //////////////////////////////////////////////////////////////////////////////////////////////////// // THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // From e8e1cb892163c97623f443f11fbb29f6a0f8abc8 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 17 Mar 2019 17:36:48 +0100 Subject: [PATCH 23/59] added boiler ww comfort., via telnet & MQTT --- lib/MyESP/MyESP.cpp | 70 ++++++++------- lib/MyESP/MyESP.h | 2 +- src/ems-esp.ino | 211 ++++++++++++++++++++++++++------------------ src/ems.cpp | 2 +- src/my_config.h | 1 + src/version.h | 2 +- 6 files changed, 167 insertions(+), 121 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index 090d0aa2b..8f94b19dc 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -806,31 +806,38 @@ unsigned long MyESP::_getUptime() { // print out ESP system stats // for battery power is ESP.getVcc() void MyESP::showSystemStats() { - myDebug_P(PSTR("[APP] %s version: %s"), _app_name, _app_version); - myDebug_P(PSTR("[APP] MyESP version: %s"), MYESP_VERSION); - myDebug_P(PSTR("[APP] Build timestamp: %s"), _buildTime().c_str()); +#if defined(ESP8266) + myDebug_P(PSTR("%sESP8266 System stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); +#else + myDebug_P(PSTR("ESP32 System stats:")); +#endif + myDebug_P(PSTR("")); + + myDebug_P(PSTR(" [APP] %s version: %s"), _app_name, _app_version); + myDebug_P(PSTR(" [APP] MyESP version: %s"), MYESP_VERSION); + myDebug_P(PSTR(" [APP] Build timestamp: %s"), _buildTime().c_str()); if (_boottime != NULL) { - myDebug_P(PSTR("[APP] Boot time: %s"), _boottime); + myDebug_P(PSTR(" [APP] Boot time: %s"), _boottime); } - myDebug_P(PSTR("[APP] Uptime: %d seconds"), _getUptime()); - myDebug_P(PSTR("[APP] System Load: %d%%"), getSystemLoadAverage()); + myDebug_P(PSTR(" [APP] Uptime: %d seconds"), _getUptime()); + myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage()); if (isAPmode()) { - myDebug_P(PSTR("[WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + myDebug_P(PSTR(" [WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); } else { - myDebug_P(PSTR("[WIFI] WiFi Hostname: %s"), _getESPhostname().c_str()); - myDebug_P(PSTR("[WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); - myDebug_P(PSTR("[WIFI] WiFi signal strength: %d%%"), getWifiQuality()); + myDebug_P(PSTR(" [WIFI] WiFi Hostname: %s"), _getESPhostname().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi signal strength: %d%%"), getWifiQuality()); } - myDebug_P(PSTR("[WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); #ifdef CRASH char output_str[80] = {0}; char buffer[16] = {0}; /* Crash info */ - myDebug_P(PSTR("[EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); - strlcpy(output_str, PSTR("[EEPROM] EEPROM Sector pool size is "), sizeof(output_str)); + myDebug_P(PSTR(" [EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); + strlcpy(output_str, PSTR(" [EEPROM] EEPROM Sector pool size is "), sizeof(output_str)); strlcat(output_str, itoa(EEPROMr.size(), buffer, 10), sizeof(output_str)); strlcat(output_str, PSTR(", and in use are: "), sizeof(output_str)); for (uint32_t i = 0; i < EEPROMr.size(); i++) { @@ -841,39 +848,40 @@ void MyESP::showSystemStats() { #endif #ifdef ARDUINO_BOARD - myDebug_P(PSTR("[SYSTEM] Board: %s"), ARDUINO_BOARD); + myDebug_P(PSTR(" [SYSTEM] Board: %s"), ARDUINO_BOARD); #endif - myDebug_P(PSTR("[SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); - myDebug_P(PSTR("[SYSTEM] SDK version: %s"), ESP.getSdkVersion()); + myDebug_P(PSTR(" [SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); + myDebug_P(PSTR(" [SYSTEM] SDK version: %s"), ESP.getSdkVersion()); #if defined(ESP8266) - myDebug_P(PSTR("[SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); - myDebug_P(PSTR("[SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); - myDebug_P(PSTR("[SYSTEM] Boot version: %d"), ESP.getBootVersion()); - myDebug_P(PSTR("[SYSTEM] Boot mode: %d"), ESP.getBootMode()); + myDebug_P(PSTR(" [SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); + myDebug_P(PSTR(" [SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); + myDebug_P(PSTR(" [SYSTEM] Boot version: %d"), ESP.getBootVersion()); + myDebug_P(PSTR(" [SYSTEM] Boot mode: %d"), ESP.getBootMode()); //myDebug_P(PSTR("[SYSTEM] Firmware MD5: %s"), (char *)ESP.getSketchMD5().c_str()); #endif FlashMode_t mode = ESP.getFlashChipMode(); #if defined(ESP8266) - myDebug_P(PSTR("[FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); + myDebug_P(PSTR(" [FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); #endif - myDebug_P(PSTR("[FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); - myDebug_P(PSTR("[FLASH] Flash mode: %s"), + myDebug_P(PSTR(" [FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); + myDebug_P(PSTR(" [FLASH] Flash mode: %s"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); #if defined(ESP8266) - myDebug_P(PSTR("[FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); + myDebug_P(PSTR(" [FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); #endif - myDebug_P(PSTR("[FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); - myDebug_P(PSTR("[FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); - myDebug_P(PSTR("[MEM] Firmware size: %d"), ESP.getSketchSize()); - myDebug_P(PSTR("[MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); - myDebug_P(PSTR("[MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); - myDebug_P(PSTR("[MEM] Free Heap: %d"), ESP.getFreeHeap()); + myDebug_P(PSTR(" [FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); + myDebug_P(PSTR(" [FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR(" [MEM] Firmware size: %d"), ESP.getSketchSize()); + myDebug_P(PSTR(" [MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); + myDebug_P(PSTR(" [MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR(" [MEM] Free Heap: %d"), ESP.getFreeHeap()); #if defined(ESP8266) - myDebug_P(PSTR("[MEM] Stack: %d"), ESP.getFreeContStack()); + myDebug_P(PSTR(" [MEM] Stack: %d"), ESP.getFreeContStack()); #endif + myDebug_P(PSTR("")); } // handler for Telnet diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 82a6e8ef2..d2837453f 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -112,7 +112,7 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); typedef struct { bool set; // is it a set command - char key[40]; + char key[50]; char description[100]; } command_t; diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 8aacf17fe..ce967b881 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -32,10 +32,8 @@ DS18 ds18; #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) // timers, all values are in seconds -#define PUBLISHVALUES_TIME 120 // every 2 minutes publish MQTT values +#define DEFAULT_PUBLISHVALUES_TIME 120 // every 2 minutes publish MQTT values, including Dallas sensors Ticker publishValuesTimer; - -#define PUBLISHSENSORVALUES_TIME 180 // every 3 minutes publish MQTT sensor values Ticker publishSensorValuesTimer; #define SYSTEMCHECK_TIME 20 // every 20 seconds check if Boiler is online @@ -66,13 +64,14 @@ typedef struct { uint8_t dallas_sensors; // count of dallas sensors // custom params - bool shower_timer; // true if we want to report back on shower times - bool shower_alert; // true if we want the alert of cold water - bool led_enabled; // LED on/off - bool test_mode; // test mode to stop automatic Tx on/off - uint8_t led_gpio; - uint8_t dallas_gpio; - uint8_t dallas_parasite; + bool shower_timer; // true if we want to report back on shower times + bool shower_alert; // true if we want the alert of cold water + bool led_enabled; // LED on/off + bool test_mode; // test mode to stop automatic Tx on/off + uint16_t publish_time; // frequency of MQTT publish in seconds + uint8_t led_gpio; + uint8_t dallas_gpio; + uint8_t dallas_parasite; } _EMSESP_Status; typedef struct { @@ -91,12 +90,13 @@ command_t PROGMEM project_cmds[] = { {true, "dallas_parasite ", "set to on if powering Dallas via parasite"}, {true, "thermostat_type ", "set the thermostat type id (e.g. 10 for 0x10)"}, {true, "boiler_type ", "set the boiler type id (e.g. 8 for 0x08)"}, - {true, "test_mode ", "test_mode turns off all automatic reads"}, - {true, "shower_timer ", "notify on shower durations"}, + {true, "test_mode ", "test_mode turns on/off all automatic reads"}, + {true, "shower_timer ", "notify via MQTT all shower durations"}, {true, "shower_alert ", "send a warning of cold water after shower time is exceeded"}, {false, "info", "show data captured on the EMS bus"}, {false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, {false, "publish", "publish all values to MQTT"}, + {false, "publish_time ", "set frequency for MQTT publishing of values"}, {false, "types", "list supported EMS telegram type IDs"}, {false, "queue", "show current Tx queue"}, {false, "autodetect", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, @@ -108,7 +108,8 @@ command_t PROGMEM project_cmds[] = { {false, "thermostat scan ", "do a read on all type IDs"}, {false, "boiler read ", "send read request to boiler"}, {false, "boiler wwtemp ", "set boiler warm water temperature"}, - {false, "boiler tapwater ", "set boiler warm tap water on/off"} + {false, "boiler tapwater ", "set boiler warm tap water on/off"}, + {false, "boiler comfort ", "set boiler warm water comfort setting"} }; @@ -262,7 +263,7 @@ void showInfo() { char buffer_type[128] = {0}; - myDebug("%sEMS-ESP System stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug("%sEMS-ESP system stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); _EMS_SYS_LOGGING sysLog = ems_getLogging(); if (sysLog == EMS_SYS_LOGGING_BASIC) { myDebug(" System logging set to Basic"); @@ -283,7 +284,7 @@ void showInfo() { ((EMSESP_Status.shower_timer) ? "enabled" : "disabled"), ((EMSESP_Status.shower_alert) ? "enabled" : "disabled")); - myDebug("\n%sEMS Bus Stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug("\n%sEMS Bus stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug(" Bus Connected=%s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d", (ems_getBusConnected() ? "yes" : "no"), EMS_Sys_Status.emsRxPgks, @@ -300,11 +301,11 @@ void showInfo() { // active stats if (ems_getBusConnected()) { if (EMS_Boiler.tapwaterActive != EMS_VALUE_INT_NOTSET) { - myDebug(" Hot tap water is %s", EMS_Boiler.tapwaterActive ? "running" : "off"); + myDebug(" Hot tap water: %s", EMS_Boiler.tapwaterActive ? "running" : "off"); } if (EMS_Boiler.heatingActive != EMS_VALUE_INT_NOTSET) { - myDebug(" Central Heating is %s", EMS_Boiler.heatingActive ? "active" : "off"); + myDebug(" Central Heating: %s", EMS_Boiler.heatingActive ? "active" : "off"); } } @@ -312,11 +313,11 @@ void showInfo() { _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { - myDebug(" Warm Water comfort is set to Hot"); + myDebug(" Warm Water comfort setting: Hot"); } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { - myDebug(" Warm Water comfort is set to Eco"); + myDebug(" Warm Water comfort setting: Eco"); } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { - myDebug(" Warm Water comfort is set to Intelligent"); + myDebug(" Warm Water comfort setting: Intelligent"); } _renderIntValue("Warm Water selected temperature", "C", EMS_Boiler.wWSelTemp); @@ -701,6 +702,12 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { recreate_config = true; } + // publish_time + if (!(EMSESP_Status.publish_time = json["publish_time"])) { + EMSESP_Status.publish_time = DEFAULT_PUBLISHVALUES_TIME; // default value + recreate_config = true; + } + return recreate_config; // return false if some settings are missing and we need to rebuild the file } @@ -714,6 +721,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { json["test_mode"] = EMSESP_Status.test_mode; json["shower_timer"] = EMSESP_Status.shower_timer; json["shower_alert"] = EMSESP_Status.shower_alert; + json["publish_time"] = EMSESP_Status.publish_time; return true; } @@ -823,6 +831,12 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug("Error. Usage: set shower_alert "); } } + + // publish_time + if ((strcmp(setting, "publish_time") == 0) && (wc == 2)) { + EMSESP_Status.publish_time = atoi(value); + ok = true; + } } if (action == MYESP_FSACTION_LIST) { @@ -848,6 +862,7 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug(" test_mode=%s", EMSESP_Status.test_mode ? "on" : "off"); myDebug(" shower_timer=%s", EMSESP_Status.shower_timer ? "on" : "off"); myDebug(" shower_alert=%s", EMSESP_Status.shower_alert ? "on" : "off"); + myDebug(" publish_time=%d", EMSESP_Status.publish_time); } return ok; @@ -896,83 +911,87 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } // shower settings - if (strcmp(first_cmd, "shower") == 0) { - if (wc == 2) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "timer") == 0) { - EMSESP_Status.shower_timer = !EMSESP_Status.shower_timer; - myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0"); - ok = true; - } else if (strcmp(second_cmd, "alert") == 0) { - EMSESP_Status.shower_alert = !EMSESP_Status.shower_alert; - myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0"); - ok = true; - } + if ((strcmp(first_cmd, "shower") == 0) && (wc == 2)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "timer") == 0) { + EMSESP_Status.shower_timer = !EMSESP_Status.shower_timer; + myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0"); + ok = true; + } else if (strcmp(second_cmd, "alert") == 0) { + EMSESP_Status.shower_alert = !EMSESP_Status.shower_alert; + myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0"); + ok = true; } } // logging - if (strcmp(first_cmd, "log") == 0) { - if (wc == 2) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "v") == 0) { - ems_setLogging(EMS_SYS_LOGGING_VERBOSE); - ok = true; - } else if (strcmp(second_cmd, "b") == 0) { - ems_setLogging(EMS_SYS_LOGGING_BASIC); - ok = true; - } else if (strcmp(second_cmd, "t") == 0) { - ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); - ok = true; - } else if (strcmp(second_cmd, "r") == 0) { - ems_setLogging(EMS_SYS_LOGGING_RAW); - ok = true; - } else if (strcmp(second_cmd, "n") == 0) { - ems_setLogging(EMS_SYS_LOGGING_NONE); - ok = true; - } + if ((strcmp(first_cmd, "log") == 0) && (wc == 2)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "v") == 0) { + ems_setLogging(EMS_SYS_LOGGING_VERBOSE); + ok = true; + } else if (strcmp(second_cmd, "b") == 0) { + ems_setLogging(EMS_SYS_LOGGING_BASIC); + ok = true; + } else if (strcmp(second_cmd, "t") == 0) { + ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); + ok = true; + } else if (strcmp(second_cmd, "r") == 0) { + ems_setLogging(EMS_SYS_LOGGING_RAW); + ok = true; + } else if (strcmp(second_cmd, "n") == 0) { + ems_setLogging(EMS_SYS_LOGGING_NONE); + ok = true; } } // thermostat commands - if (strcmp(first_cmd, "thermostat") == 0) { - if (wc == 3) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "temp") == 0) { - ems_setThermostatTemp(_readFloatNumber()); - ok = true; - } else if (strcmp(second_cmd, "mode") == 0) { - ems_setThermostatMode(_readIntNumber()); - ok = true; - } else if (strcmp(second_cmd, "read") == 0) { - ems_doReadCommand(_readHexNumber(), EMS_Thermostat.type_id); - ok = true; - } else if (strcmp(second_cmd, "scan") == 0) { - startThermostatScan(_readIntNumber()); - ok = true; - } + if ((strcmp(first_cmd, "thermostat") == 0) && (wc == 3)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "temp") == 0) { + ems_setThermostatTemp(_readFloatNumber()); + ok = true; + } else if (strcmp(second_cmd, "mode") == 0) { + ems_setThermostatMode(_readIntNumber()); + ok = true; + } else if (strcmp(second_cmd, "read") == 0) { + ems_doReadCommand(_readHexNumber(), EMS_Thermostat.type_id); + ok = true; + } else if (strcmp(second_cmd, "scan") == 0) { + startThermostatScan(_readIntNumber()); + ok = true; } } // boiler commands - if (strcmp(first_cmd, "boiler") == 0) { - if (wc == 3) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "wwtemp") == 0) { - ems_setWarmWaterTemp(_readIntNumber()); + if ((strcmp(first_cmd, "boiler") == 0) && (wc == 3)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "wwtemp") == 0) { + ems_setWarmWaterTemp(_readIntNumber()); + ok = true; + } else if (strcmp(second_cmd, "comfort") == 0) { + char * third_cmd = _readWord(); + if (strcmp(third_cmd, "hot") == 0) { + ems_setWarmWaterModeComfort(1); ok = true; - } else if (strcmp(second_cmd, "read") == 0) { - ems_doReadCommand(_readHexNumber(), EMS_Boiler.type_id); + } else if (strcmp(third_cmd, "eco") == 0) { + ems_setWarmWaterModeComfort(2); + ok = true; + } else if (strcmp(third_cmd, "intelligent") == 0) { + ems_setWarmWaterModeComfort(3); + ok = true; + } + } else if (strcmp(second_cmd, "read") == 0) { + ems_doReadCommand(_readHexNumber(), EMS_Boiler.type_id); + ok = true; + } else if (strcmp(second_cmd, "tapwater") == 0) { + char * third_cmd = _readWord(); + if (strcmp(third_cmd, "on") == 0) { + ems_setWarmTapWaterActivated(true); + ok = true; + } else if (strcmp(third_cmd, "off") == 0) { + ems_setWarmTapWaterActivated(false); ok = true; - } else if (strcmp(second_cmd, "tapwater") == 0) { - char * third_cmd = _readWord(); - if (strcmp(third_cmd, "on") == 0) { - ems_setWarmTapWaterActivated(true); - ok = true; - } else if (strcmp(third_cmd, "off") == 0) { - ems_setWarmTapWaterActivated(false); - ok = true; - } } } } @@ -1009,6 +1028,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE); myESP.mqttSubscribe(TOPIC_BOILER_WWACTIVATED); myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_COMFORT); myESP.mqttSubscribe(TOPIC_SHOWER_TIMER); myESP.mqttSubscribe(TOPIC_SHOWER_ALERT); myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT); @@ -1070,6 +1090,19 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { publishValues(true); // publish back immediately } + // boiler ww comfort setting + if (strcmp(topic, TOPIC_BOILER_CMD_COMFORT) == 0) { + myDebug("MQTT topic: boiler warm water comfort value is %s", message); + if (strcmp((char *)message, "hot") == 0) { + ems_setWarmWaterModeComfort(1); + } else if (strcmp((char *)message, "comfort") == 0) { + ems_setWarmWaterModeComfort(2); + } else if (strcmp((char *)message, "intelligent") == 0) { + ems_setWarmWaterModeComfort(3); + } + // publishValues(true); // publish back immediately + } + // shower timer if (strcmp(topic, TOPIC_SHOWER_TIMER) == 0) { if (message[0] == '1') { @@ -1113,12 +1146,14 @@ void WIFICallback() { } // Initialize the boiler settings and shower settings +// Most of these will be overwritten after the SPIFFS config file is loaded void initEMSESP() { // general settings EMSESP_Status.shower_timer = false; EMSESP_Status.shower_alert = false; EMSESP_Status.led_enabled = true; // LED is on by default EMSESP_Status.test_mode = false; + EMSESP_Status.publish_time = DEFAULT_PUBLISHVALUES_TIME; EMSESP_Status.timestamp = millis(); EMSESP_Status.dallas_sensors = 0; @@ -1316,11 +1351,13 @@ void setup() { // start up all the services myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION); + // at this point we have the settings from our internall SPIFFS config file + // enable regular checks if not in test mode if (!EMSESP_Status.test_mode) { - publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post MQTT values - regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS - publishSensorValuesTimer.attach(PUBLISHSENSORVALUES_TIME, do_publishSensorValues); // post MQTT sensor values + publishValuesTimer.attach(EMSESP_Status.publish_time, do_publishValues); // post MQTT EMS values + publishSensorValuesTimer.attach(EMSESP_Status.publish_time, do_publishSensorValues); // post MQTT sensor values + regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS } // set pin for LED @@ -1360,5 +1397,5 @@ void loop() { showerCheck(); } - delay(1); // some time to WiFi and everything else to catch up + delay(1); // some time to WiFi and everything else to catch up, and prevent overheating } diff --git a/src/ems.cpp b/src/ems.cpp index 7e68e3d51..a875b4d72 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -1740,7 +1740,7 @@ void ems_setWarmWaterModeComfort(uint8_t comfort) { EMS_Sys_Status.txRetryCount = 0; // reset retry counter if (comfort == 1) { - myDebug("Setting boiler warm water comfort mode to Comfort"); + myDebug("Setting boiler warm water comfort mode to Hot"); EMS_TxTelegram.dataValue = EMS_VALUE_UBAParameterWW_wwComfort_Hot; } else if (comfort == 2) { myDebug("Setting boiler warm water comfort mode to Eco"); diff --git a/src/my_config.h b/src/my_config.h index 25d771df3..9ff43a405 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -37,6 +37,7 @@ #define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on #define TOPIC_BOILER_WWACTIVATED "wwactivated" // for receiving MQTT message to change water on/off #define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT +#define TOPIC_BOILER_CMD_COMFORT "boiler_cmd_comfort" // for received boiler ww comfort setting via MQTT // shower time #define TOPIC_SHOWERTIME "showertime" // for sending shower time results diff --git a/src/version.h b/src/version.h index ff30e1dab..366b539e4 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.6.0b2" +#define APP_VERSION "1.6.0b3" #define APP_HOSTNAME "ems-esp" From 1d261ad9d361be857fd8976cd135a3f6f8764571 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 17 Mar 2019 21:38:05 +0100 Subject: [PATCH 24/59] added ms to verbose logging --- src/ems.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index a875b4d72..6f0b23e28 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -391,6 +391,17 @@ char * _smallitoa(uint8_t value, char * buffer) { return buffer; } +/* for decimals 0 to 999, printed as a string + * From Simon Arlott @nomis + */ +char * _smallitoa3(uint16_t value, char * buffer) { + buffer[0] = ((value / 100) == 0) ? '0' : (value / 100) + '0'; + buffer[1] = (((value % 100) / 10) == 0) ? '0' : ((value % 100) / 10) + '0'; + buffer[2] = (value % 10) + '0'; + buffer[3] = '\0'; + return buffer; +} + /** * debug print a telegram to telnet/serial including the CRC * len is length in bytes including the CRC @@ -403,6 +414,7 @@ void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const char buffer[16] = {0}; unsigned long upt = millis(); + strlcpy(output_str, "(", sizeof(output_str)); strlcat(output_str, COLOR_CYAN, sizeof(output_str)); strlcat(output_str, _smallitoa((uint8_t)((upt / 3600000) % 24), buffer), sizeof(output_str)); @@ -410,6 +422,8 @@ void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const strlcat(output_str, _smallitoa((uint8_t)((upt / 60000) % 60), buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); strlcat(output_str, _smallitoa((uint8_t)((upt / 1000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, ".", sizeof(output_str)); + strlcat(output_str, _smallitoa3(upt % 1000, buffer), sizeof(output_str)); strlcat(output_str, COLOR_RESET, sizeof(output_str)); strlcat(output_str, ") ", sizeof(output_str)); @@ -561,7 +575,7 @@ void _createValidate() { */ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // check if we just received a single byte - // it could well be a Poll request from the boiler to us which will have a value of 0x8B (0x0B | 0x80) + // it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) // or either a return code like 0x01 or 0x04 from the last Write command if (length == 1) { uint8_t value = telegram[0]; // 1st byte of data package @@ -571,7 +585,8 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { EMS_Sys_Status.emsPollTimestamp = millis(); // store when we received a last poll EMS_Sys_Status.emsTxCapable = true; - // do we have something to send thats waiting in the Tx queue? if so send it if the Queue is not in a wait state + // do we have something to send thats waiting in the Tx queue? + // if so send it if the Queue is not in a wait state if ((!EMS_TxQueue.isEmpty()) && (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE)) { _ems_sendTelegram(); // perform the read/write command immediately } else { @@ -930,8 +945,6 @@ void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length) /** * UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long) * received every 10 seconds - * e.g. 08 00 18 00 4B 01 67 02 00 01 01 40 40 01 4B 80 00 01 4A 00 00 0E 30 45 01 09 00 00 00 (CRC=04), #data=25 - * 08 00 18 00 4B 01 56 03 00 01 01 40 40 01 3E 80 00 01 4D 00 00 0E 30 45 01 09 00 00 00 (CRC=EA), #data=25 */ void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.selFlowTemp = data[0]; From 2701c8a0df4a3467c1895e7ef7f64dc04402de27 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 17 Mar 2019 22:40:13 +0100 Subject: [PATCH 25/59] change verbose logging to find duplicates --- src/ems.cpp | 6 +++--- src/ems.h | 4 ++-- src/emsuart.h | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index 6f0b23e28..1724e2b76 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -410,7 +410,7 @@ void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const if (EMS_Sys_Status.emsLogging <= EMS_SYS_LOGGING_BASIC) return; - char output_str[300] = {0}; // roughly EMS_MAX_TELEGRAM_LENGTH*3 + 20 + char output_str[200] = {0}; char buffer[16] = {0}; unsigned long upt = millis(); @@ -668,7 +668,7 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { // print detailed telegram data if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { - char output_str[300] = {0}; // roughly EMS_MAX_TELEGRAM_LENGTH*3 + 20 + char output_str[200] = {0}; char buffer[16] = {0}; char color_s[20] = {0}; @@ -740,7 +740,7 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { if (typeFound && (commonType || forUs)) { if ((EMS_Types[i].processType_cb) != (void *)NULL) { // print non-verbose message - if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) || (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE)) { + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) { myDebug("<--- %s(0x%02X) received", EMS_Types[i].typeString, type); } // call callback function to process it diff --git a/src/ems.h b/src/ems.h index e5e4af523..0b4dc20be 100644 --- a/src/ems.h +++ b/src/ems.h @@ -19,8 +19,8 @@ #define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC -// max length of a telegram, including CRC, for Rx and Tx. -#define EMS_MAX_TELEGRAM_LENGTH 99 +// max length of a telegram, including CRC, for Rx and Tx. Data size is 32, so reserving 40 to be safe +#define EMS_MAX_TELEGRAM_LENGTH 40 // default values #define EMS_VALUE_INT_ON 1 // boolean true diff --git a/src/emsuart.h b/src/emsuart.h index e8e371909..e1fc9498c 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -10,15 +10,15 @@ #include #define EMSUART_UART 0 // UART 0 -#define EMSUART_CONFIG 0x1c // 8N1 (8 bits, no stop bits, 1 parity) +#define EMSUART_CONFIG 0x1C // 8N1 (8 bits, no stop bits, 1 parity) #define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit -#define EMS_MAXBUFFERS 4 // 4 buffers for circular filling to avoid collisions -#define EMS_MAXBUFFERSIZE 128 // max size of the buffer. packets are max 32 bytes +#define EMS_MAXBUFFERS 10 // 4 buffers for circular filling to avoid collisions +#define EMS_MAXBUFFERSIZE 32 // max size of the buffer. packets are max 32 bytes -// this is how long we drop the Tx signal to create a 11-bit Break of zeros +// this is how long we drop the Tx signal to create a 11-bit Break of zeros (BRK) // At 9600 baud, 11 bits will be 1144 microseconds -// the BRK from Boiler is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag) +// the BRK from Boiler master is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag) #define EMS_TX_BRK_WAIT 2070 #define EMSUART_recvTaskPrio 1 From f3a53f74cbf48858425d1eba4ca3919bf8d06447 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 18 Mar 2019 22:31:33 +0100 Subject: [PATCH 26/59] updated screenshots --- CHANGELOG.md | 1 + doc/telnet/telnet_menu.jpg | Bin 139774 -> 68345 bytes doc/telnet/telnet_stats.PNG | Bin 45917 -> 45112 bytes 3 files changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f933f965..83db7685f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - included various fixes and suggestions from @nomis - upgraded MyESP library +- test_mode renamed to silent_mode ## [1.5.6] 2019-03-09 diff --git a/doc/telnet/telnet_menu.jpg b/doc/telnet/telnet_menu.jpg index 9355e493faa6f598efbec9cc906ff5d0dbfe6052..563fdd8bac40f29e7f3732d5aa12d369ddfe3bf6 100644 GIT binary patch literal 68345 zcmeFZ1yo#3n=ab8yF0;yyF0-`aM$3_SmW**2u^_D1b26LcMWvo79hA2vWM=*K3qS?{FddIb0SIr$P~Xy` zZvr4adiz5BEB1Hz8-c$O_#1)05%?Q{zY+L95djqwH@i2xq5h2{Ae#P#!`~d?|HRht zx6OYe@HYZ~Bk(r@e_P9SOev}oh{V8l{Cz} zZOsJC>BU6RkcB-3J?$OsEnH0~J?-rrTm(HusQ!_=;9LB!!)#Qd!p`QFf@&XR{;Bbn z5~2F%y?A(duzGN@f}E|`I0OU)*w{JQI5}C~G+11`99&I2SsYxb|CzxD3l}qIprb1g zucsBf5Y{kLf}88{CDg6-wdvQqly*|Z~V~XjR?Mi z0a5@2I5>DXSOj=DctivQL}V*SF)bZE!+S<<9$r3v z0YRw`(lWAg@*mYTG_|yKboI>4EiA39fi^C#ZtfnQUfv<0Vc`*9BBK(Mz9pxmeosr! z%P%M_DlRE4tE+ElY-(<4ZR_hF7#tcN86BIOUszmP{<*TcwY{^uw}0^K@aW?5>iXvP z?*8HNFT0=sF#lrKzgqTxu?y?XE@)U-7+8eA?1F;!coP^bSh#l_@YoWn2qsQARGdMG zxRMD!YI~8Wxzx__Or2+u@oBiXXfOUU?H`u?#|#VpFIo1lhW(pes{m9OsJF_4!2*Z@ zu59CO?60eAZpZs0H;;Y3DpanP^G#Ev4`N_@lXz*68&hV6 zj|d3l=f)sD(H1M9d`7d>oEPm|HnpsKJ_K_Pm~lk(2sohsCg+spr|9!`t6PVBw&jwx zaf&SFwL##_=7f3pD-_sI>v0B!X~do^WhuC zH`JWc5(l12$)lB%gO)X%FH)?7M3*WBeh3ycK6jHm4&6)LyA;cNuK?IX`!0$f=_foa zmg}SMI#Z_Jn|rDV$;j-R(3D6%jo(RSceqY*ud-yNM8fUom&m2^0jXEPU~r^nvQClV zc-4%qJ|SAqB-MSrHkzw>`ue7!(lU1CWF{Be_ccS!nK1e;h%0$;`Ed2;;esqaw~mx< z-o^B#_rxL_K4x_(pLWFFlU^+q>!U6%T~{J+NX_oevx4+4Pk(l;u;6K_9|&UWPYTJQ z1Og~G$&vV5{m!5C4fI!fD^?Z!?|`=4@H$FE0^2MYam%er8fzw2aVA z+)}ltPrxUX+O1$+q4(Y)k$}le2nm|hSQM5jwdFn6-#p3=sf%uV2NM_jld zXwF50HO8D=l<*U0!ruWGP?^+m+as_|a)OV>rMAcny1|pLh&bhyoM6>=?0E%P?a|ix zaY#2u-U!K`ng>;C7$gHtc-gsWQ%`HY6b(`@0K7zt%41yAus^r9d-zEs8@2~dGBA@Q zdXWY?GZ5T72YhytY-iC{nq#^4V`T)T9XjuCotus|c;x5V^Q!7p3OqcJ2P zN649NFh8Y#WVI!NvMfGdlq-X1q>+ZDp+`N)5mtKmCrQ2G+4x@Q#ot3BA2U zLS&(w*>2ex$x7E)9$yBn#X?tZ$ZLp0rm*Vs(X2EcxUZ&9Ue5kxCzxGXYlXOMche>M zT!c~0U7M-AwsxzMc+ss=2F>2}A!TWBAB}cg0bo~!Wa~~7%_mKtp86?5BKwcJwjz~6 zo{COCbF->0fA2?ye|{1@(o?^zZ>blU3C!BvPN2sW+H zt!2Qru+hZ1A(B)_i&%R)$b%g`J63+59{~69saw@m(2q&HHhl22;T-o*<=cweyGb|y zna&Ei+RP7K*6T9+!`L{(23NhGsBuOpnN1?HUguB68QlJNOv5M=8q$oPA5V z|MJ8TDzL4YU?~R1dI89lOG~=9s1{=;2++LGxC!iiNMnC||Kz?mN}bcvK^n{Od3o!t zO9g3d+$_I9rH^SwW#?`#OL*B9iv?Dko!E7J{o#9iIbmtV8ARM9_a{GvzvnPIX{q(r zbVBD+eza2FSH5KO2W#vr!2Az(yd+CBa&OB+bLd(-0D5JVOp(HJJ?A@c(mzxXKb_Opu4yc!=71G46W-CX~R`rjqw;z&4qJnziCYOy!d52ZLH+Tm(%X|jD!SsVPw-r+ArAlI>*|2^^k&8kHD`XG+1pP(K(4Z z3lT>U2#=sn)cUtiOi{j;hMs?1R*GY2sSme+1ct6wh0@K)L5cTls?PAXHWyfXyER9G zgvH7Riw(!gdc)t7RpEP$!ZHpvh8iDG9P^L6@eJ103CP0fDj|x**ywv^Xfy(|-{)~n zQ-8cu)0d|0aajzVC|tN3F$tO08#`V~$)91!i!w!K$P4`ME+TY0&-#8$>75s}VbdX% z*Wa%790%iSwXNdZk*s!xqilX zP+wqRl4eK~;zVm}TNXgrOJic3UR;fQP?^xmpG_3V!Uj_4&KdCgEP@J|0q8yP9Lsr0 zIW|7!0s49F9xqK+P6QDZv5m&5Mnr82r4`u;6tj1oPVV5QMQu}+snv)f- zYG&vhbz4?l6Q?k>O1X8KzUD}+k52n@!9wL`)qytZOha9ElDUNAr^Wit6&)<9 z;sxu5EAoBm`0?!7H}>+bf1S(_Uo%cl&PpFhXy})xCZXU-yVlTzw8rH=gz)J~H=B3H zHH$(4Y*J{$!KqXVpL-i!V=&xswV#${kdquFEwLQipV~6RcEY{T)rx;1O~q6J^1%FF z?zeQ=4C+13>J&Jf)D1wiMWT)NVWIJp2nI9jg<^YUCgdbf!D~ck0%W+-Y0Gh4KM+4x z|2+E<_lNI~kXr@Yp4W@R^9V*-Hn_eWWVhWQj8m{;vzj%xA;kigu(JrCB8;wd1b@TC zGLOkX)~%_)AlijSH!A+IafpPPT03Ogf-shjw3E%(+Y7a0X%NfFm5^}6BpTYEC&D-{ zb^fbmVLbq24KDJW<*Wk?e8&W2o`8-d4AkR@8T_KYQAy*ESDXDuBe-8;R2O&9#SL$M z1zZ}bQ<}mzhN+X;wH0k%P4h6puHeERg}5r~5S34}l;LTm7Belh%Eot~M~MC!qf*^X*c2$Z0=@ z?wR?|L&7~L2U}ESptP4Jt&b~cJI&NG+N^tuAnND6({ruccI4zYK0YL7F26dtEmp`N z9gHXvNk)^uDS{fDMei1ime7~%_dmiGdv!lV^OBxJP%lm0t<`XVf1DiGUMLr&huloWp30pA@Yb3n$*dEVUlZTFRb~Q~a>j0>QZTJzZp~Jt5dz+U za^Xn&2kjSMwEa)Bercb%SOG#&J7EGnqPdHzsG+Zba?n_rR?6&keyS0%8pXF;>m7G`gAPta}zP!aE5NGhr;F5SHZ0?t#xZX*ep43q^qn=I4?d zz+}Ga@5ZMFfihb&AmPoZ%W*JMjNqfK!pKwWmUAMjL@xqDLhsXi78d&Mj;N(MF6JwK zxfu_-^ktCP@Z&?`VuO|L_N3s>06CAfYdOd5MlDUVBT%TuvJu2 z&u#kRY0x%_pXk_;kANMi|2Cv1(QtN40V;*kJZzeX23^tV&Jjg_)EH;bk=nV-j^>-M zhno1vU8!A#C>RZrW82vLhgsBNWp{P%5`KYV0CGScIQNHbQf$CaJ301BA~T?ms@lD+ zF(nl(W3P}M(0}fe9#W;|^zFgM53n~%8wy?pFab?M4?K$T?B(y*w zlX;D^U+7EHoMJ>*$=*$Lmd{t4XKnbp1N8k;7pnaXDW^)d zKrP@*M5GMq%@x!smo}Wc{yJ$2G_BQ%4Ci8w;lAiA2@Fb{>Oq$~TQ3UfUk#hQzO7+t z&u}CLhCz%z7+=%buj@=X-L(q4<)V%;Sz;g_Yx8dV*nUXo;!7kJ`U0kRU+YI3vx<2I zc&wLZF$Vl@1oF#!HI5STd!vBl=Omi9QT z4ahIdUzZOQ=3!qcF)67}ulYBhI%@h!Gm{Jphaa0Bwb{Hj|CEa%4_0>eaXXttOTLF; zY+5T$-Z>*z!ripDoFqe7DBZIC#eIecywgrZtlatTRV5*$ajpcQ|M8p45O1lr@Hu6Q z4dW=m`ice2r4-5SuQwDO`Hms1?l5|X-pN#a(z%T;ohQb9T#56$^u##PH@fG*?3R%! z(Z*Sfh|`3JX5y$pT;~Y~l2Q8cV0O80s8546jva1Bj?QGitDjw8y0;>apS_YG$LW4? z(H+vID@VM|2}hVpOTX>YrCBebu@ZI8l@J`Y*GuoUwNXlsxQ>X|x{~1*8*0X(2Wgz9 zjOeAGAy~`3QG5#;o%Yozw76^@SL>d71uR316^RFhh7t?L zD#R0@karf$VM0!!s;ZT`=E41@-~r;494`RG?+;Ra1jH5N)bh7qw!`vT!V+A%sXnajzZddX z4@Adk6suz`YFxX`aoLK9(_LtI{&>re#cZ^+Zr&Oi+|3+267>rgYt$K?H&e6Rc9Ohb z?L6}p08U9oD#l9`)xlA%2>hQ&oNU;D7g2t7z28!5P; zD%B2Nh9S2v>c+w=k0D4*THXgAoMcmVwfqUVP0irqTYbu5nmRbMdm(+)sJl1Qh)|1A z<^Xp>1_Ps*TFl=!pMh3~$A<;hqKyxJl&1Esg4ei_>~Zj#|7fQ-I}t`p1gB!3{uHN& zJ&1LgnOoa(ca2i_h~d$Wvf6UC&}3v-cjzC=XGV{1#j%{ zKh??X-rsW&4#TQU*$`}tKZ z3wbW-ShC}$Z}X?^k>ZL^2oD|h?Ouw=crk66=o!r0Ju)|caniM?Fq6c(vDNlJ;R#@) zE`0@r=F9CoGh8j-*$*G84ORi~n5moFV+tJcc`PVXPL`|ZT3W7|9=MAZK1$)0tTh(5 z#?mE$)_+#?=8(NH&Fw0ib=S=kNKF>7LZ&2!VVx9p z^6h2TFOjaOrJN2V@a7dA61mMkn4$AQb?gS~6Ov|!MCbGqcVP8R+ErO3n_%?|JC7vt zy65NBnwy1O)*u865&KtemB$dsHFmUTukE(9q);%fd+sCEs_b^y_0#+FW@5#8FA-`h z371y4cTa2$SdHUpYLp`xx@R>lKLp^kH0mZPHz=f_Io4u1_TT6D7{}RXASmU|KMkTv zafTeWb#$^?#XTF$vHhu%)-n97sv6|y6z7Fh^)b!2ke;d1x55veAliDiptQ|-zC4xd zq1kM9&fG$%Ac}7qDkoUv3w&AY!bIp03%;;Z;>1P~Q-m_6O~NyH1>?_8(1ALAWFww{ z3u?~Uhwgbiij^DuB|)*J-(=Fjt7--R*aH*RYLIoHQ9W~nm^(r5qti{rY?Ot#T3!I`%;}f zUDOurZqOvjXd`v3%vc8aU|#e4*EVm8x34? z>vj&bPNa6Nj0ILzKBg^4jCYU&J)vs#rE3Nreo<1r(Sm87ABjT;zFn>t`lRjQ9xFJq z9Sl8I$%uWs#;8|<5m7sx^K9HJJV@ljf5_)ZeEceKPLOub_=ot=>d=Nu2&Xs(V`)+paYgP|VufPDQQS$)BErBJ7rS&&Jp(BI$|^$cqE^0_?-7TjFw zO4=c^V>WN-YWBH5Fs*M$il0RKI)2$pdD4R{v5S@;%#@xNn?j(U6<%C<*yd24JDxj9 zww_U_c+l@)Bq8b%0pdyLF&qZ?1_gpQN)N6p)&Fp*C7z95dGreF^O=#G+oeqjIZSsk ziAIT4cX%L!8yBxzgjrk)$BT{Rot&sXE~9vt1oil3^MA-^!xm5-dZ zDYdU(9^ZpY4hzq!vsFHnkTQqHOCldOuDv4~CtjH+uK2S;kxfeTE!g*1H_;rz$@d=C z>auJ>y)&!7Fg9Go=VQZC%wBksbG$O?tfBt2FU@))G@=YvsTveNy@%aF8^kjYrdXj^ z2Y3RPR>xb5#3T-t(6=*#ocQU2uz)M{{+f>nmVooqnN1GG(1B zLZckMe?eo4cJU4~uz!IRJw(C0?VrYfuOW%F0H^4t?g{-~e*b9F9D<-}}H{b8l> z7$uu)twXQh(MMj%TH(!W{$i=zd)wc^H`lo4=^K-n&t=BcgB_QoWGgh^wUZJ?dk#aa zub>xyULG+map%AR3A`u!yzI z4sSt)GIxCG7JKvS4VNJ8FVpol;V7>F=0E`iQAnZ)!OiN<$H)B0yiR;YN?;f6l44bC zq~)q&J6+lIJKJI}V@s5jpW-yZc+TzO^z@nC1iv_dk1UmYRk5xudJ_2KBM$G{T^bVQ z+&S7V?vh9ok`02SW)w67j?Je8qZXD={D=Ke16I0Mz?CKeOw(5$r{ca7w`*BoA1l1n z|Mng0f3@TPXNTOU@Z%^RSYD!N;+RqyOlwXFa0(XfMR4m5$I4Z~JD7Pz7vYG^p0YnznlKqlh#XLb(LbPH4ATl~AtFBwX z`B^+w?TaxGSDyO4@z-o61bUd7{q)7eTbcz|a3|yW>PDaGH$aa$yM=JL77=QJGa?PK zsjh*%3D1#ihNrn)fnWcg6{?0=^*fTws+bOSTZ%7*={|%Gzbm)?&=xpK6H3SEGxiwB zavUU0PhOevZx?*{n*yt)@YrG8MVzV53`jQgXhIGt4*0O&CL=}Ep|yX zqB&n+ghud3gP8@mkNA~k!0u?+9fs0=JgYKT906}*#j{zyEH6@Ej~|lmI0?5wJas=f0sO|gUB-e|q$$n0WZ1(B zDM0WE!3WAT7(n!lh(=5NL*?=)`3& zx8SV&aJJg8b)H@9R5EPl@MzbJ9jI!yXs^RT4<&(ZTuXm?Ai9u^w_xvjV%_+sLF*2G zUaw;$BihK^F5_Z@XcQ*XvS@B;Sn|R@&ULl1@}z;!Ns+s0cJj-j0Z#lysW9p}nrMmA ztzFbcpJ3zK#JT=gdRuqtgrR&;y~}}3#71Le?EQp zSzJ98%4o5ou$C-v==;;QqliGZ<_hMIU!4zN^ut{ofXXL4_T~p&T{?#*DwM;mV zsie>|xrfmZ>no)IX$_h9na0xX8uxI6phf_~S~d*4uQrT!zjC(^1)#jnfh`rt^Do=`MG5J>=e(T*rpOM(df<(in+lc^PI*>! zYhCbUzMv?IDYnd~tW}!CR9O76$|C*R?8CU*gJi7l*!wQli=Mr3DWUjC6syJ1dFs%$ z70clR`*bpewU+|gMU}V~OsosIQ?%&{J(WK_dunX^#RaXv-#nP_TTG8{N#W0{UuBB! z=^kHC92~gY<1p8G76&^Q*H`Fu3H9@bg7Hb%CCkGp-@!S>Lk%i*aVHC#pq^Fs!YZ4w zG&HBXriNmVQbvzhPQ+?{$MotM5;Tqf?RA3ZqLre1Bfs0S(A`LD%4g(|(6Npn5D$O5 zi=M0$^k40O|G&QOpD%#%bx+Zw>AqT9kYfz#cQec=@v+Kg;GXB7;iNEr65V=3>3_`H zWu_Hcpt!S9m&TDkRk&7)Vw6MP=*npsv6vBY+9!8 zXsFFzV)`mpH)ijUf|4rK{E%(uRI(Az>F&%gAiRioA{R?{4Ba71zn zK#j)8H0$k^cp~G!D_|Zt4${{?Ns$+Qv?Hh}m;khdRz`85VhO zaei8kHaLK-hsDy_r#BL`ki%2or~z|@6LRNrtjBpDyR_3UpPykH;Y-Cb`8+n&)Pt!r z04uQr8`nk{jngZ|MIuk5aYg*0Gb~Sf zigPaFXUC>>lRq@)Zy)VY?ZJX{0Eu zwIp`}rE+k>0VfpSc0yaTnahlPK#4O~$Acz%pwOK%_Q*kcK`I!5Drgm45X|l!Vn}$= z&Sc~eetUL;Iw0@ocknS6Jc^F;o1}K%D)%tXJ62IaW>KlR^yq>SGU<@;6U;PXNDD>a zFOH~0wW^G?$pNmlvlKH%J3Bcn$u05W zF-K=5Z`B#G>ggA#Y+w2cl*J}?sgzI%QrLnF1~V!CRkN4pG|}2}Y=>sM7|fA2jGBHV z3kz*+kOdOYE;21-1PXNuzK`PFkj6PzBBR9iIF{{m+H(Tit=LL5xiVNRAc-<5Afw*F z18Qi(EZQpF_(HQH^-c^pPuB488@tzjyA@0&6CGVV!60FmI|>d->dAk6dIf9-=s|CM zJ_g;3Efote+oMk7Br`{Ad~+O=yQZd$a~kB$dLKQp4`&`-`?CuC{cS}bUbzclF5~|f zO!}Xl=A_Yzr;{)l$O-7yu4(zObER_?Ix^JTGy3xDIKuaBEc=%?X*F+tZeN8Cie=ZA z&+;@X4iD=LA1cH)X}^*C4QiMEKHaTpSNM*PpTHHwjl}3W%&=MdB+cKDwZdjr)IG}r z>M+@s?re)tYMw_;6sW6v&|xkoU-%j@o&|yac_T6yL*JKf;JvrKex#M8udnl5M_5gr zEheCcc{D|ZDo!#a?t3w0o85ImS6l*FgBU7cWcUyyJI?A$6QsBIUt-A$eIr}*JXR$G zsc;Dw?HnX7=+^%17{H`7%2`w4T8?dGk^@&p&2}G;zhG=SNN=6+skB4^ZxHQpRN8Iz zaci0qt#ztqbkYn2iwwO_9Xe1^uLv+j8W(EhV=e?_hChXb=m~-%8x84}f8X*U)<$sB zr@=ZCW=4Zz(&S@WNJ|ldelfA6WqlQ3o^X~IpqW3;?fmeMaeQ%mT}w@xX?xtbMB?bgXG2}u=Wv`EIACaUP$9d}w?oW6?HF_`}WOvlXir63n z-u!s}L&B8XcD}&-LL8zIzQ28ur7~uJlDrvM`zKM31`;zZBat*uoqllqQ5o~|XyKbf zWr!_atESc^^H$_8N-&IIiRCWLr~oC-B=;P8u|mZ?;}8hbaP{8%K8Hy|pFUjYV*$xs z^6K7KewAo+abc(I#*Y+;dNx|!z?mEQs=R?Cd|R>i6A4AnKcs5<*`zWLN-E;+t8>1L{of^TWC2Df~ zG||`B1x+vQY)e}UR$qh#_#*tLQq>ERKADe1Tkw|KqgFmiw2Hv-eVk)q3zuG{4V?k1 zvxy((_dnV;_?O(IMuVh;HM*xLX_cJdKvDtjtqZ1(0h2 z_%ar>NC4*oS3JSheyz?|KvnlSTAD9$Y#U0bZU&S(d54 zor>0SvqszO@Oo%c-Uo8pemv;KclS_hc-(kI25x|G22owOF!(>HV~vew4eX%He%i@2 z>yB#FUNl_@SsnJkf0D<<$rHPT;p+SFl2qJ7yFkV|`ET{dNC#}6J zPivB96Tu0pItmvzjAN3+;^*~;jGCT!GnkOk&1~oGm;IyF^>r^kX;Ax^VXi#{9H-Rr zxY+t6T>?3rT`>^ps2%u9>MT+qW1%U=fPHcWAOoz;$9`jJB-v1m@lzL{YuLcB@t0m$ zPLGLs;++?g*pnqz^X28y!o*VJjI5KApz(5miu*+U8idrf2LJ*d%I865>A+$qw9yX zBSa@makx!#aA&E2rGyM5XQjE-8hlV_63U~il+a0v6y@EvMbxe)aSRybjq!&Ve#q_U z+(oRGl;QbXAnd`=AK4_YoeVyWvp}-V3goe~a!phvOLk@e4CFb><1a|^mJwwhmBj@XB9=V6kQXVwK$wN6#t|pkN^1j92 z5~pxU{3DeIZ<4(<=N%J!rqgDxSB3xP69hzCjXUwkYP4Po8?^y>4dCu03k5jom%dcRe5NnH&eeG4r zDq2qb)t=2E1canses7izZSUJ0d&l`iHi+fJ-4&Sz9Z< z#!GYkMZ7s6wDC`EKI_o!5~~Ii{^!C^9d479vmIQfOH_xIhUu$1DPD!il@6?$3)quH z5KQPEydHiv&nd5r92^@D7kQ?}3TFkJL2CUKDfF9w&URDOq}bww)imHX${!|Ank9IrM>P5Pr@aU)88S2)MT2s zc$-YV&@JWmFiFVttB|U8t|pQEhb$)r8bC{7KaPHs#W z9rvvd#a&trSwDW9Y3RZOHR$%;8S@M#PtG!tOA>zSw?4>x5b+#3jUX*N2;XWt9}uh{ z&?c}us(jbnq#X=Bd@4_w(d z{vrqeuM5;6AH@;)&|vW_l9|<)3{)^&^x(nK^yxk>8;3sh$UR|*B&ao~(6#v=S~P9s zrSR!(p-TivHb1GfBmoG7X1~z4f;~m*qXaQZWvZ?F)df2!ep3D3*nRkHy0(8U0B+7s zI|TE{L^-*8VExidjW|f%Mu<$ml1+a+G1AXq5!FmU`w6UVfqMsmW8QdwK<0?1KmYWu z2x6oCVw6lWjp|;cfF!yosLxeSq>gE+(o+KC0Yk$v#WYcF^>Lxmk59Heanydd=4uB|n&#oNgsd2K3ppXx_=Y$+Iq;8|f zdRk3QR=syzhqJ>TB?knij(lM0Y+{;Sc6uV;J#Tx}D`GW0oAh12M@Ti<@HY=gy@3o3 zAlcx-3$2+sKdk3)wOra{|4^G?!?scR-K^-;rKv8TRkG2hSrV8kxsrsV({QR-Wxm0w za2>1dQBs^vH!!?D80(Xd}eI5DR0HZynXLbkk)y4H4apNt`@xxT+ zWEJR$Bb)O+5JP60`}P;f+wNPmx>vxQ_0e6>51*iFS^V#h6%n5v>}CWwb3{oUb2wm1 z0U?22WVV^@oh{vI7VSwBu8~yg4XBI^nxx(UI0>c4nv*?H&+3Ima#c>17rtXjsax|iROHTkS5+4f-1GbJnj;?{@K+rN(_qC&H-cpLRrEbb zWvsVzndfyu#G;ra@`d%UX4Wv4TdU_4*@}W8YfXKn~rfFbM$!RHTRdo+17= zhC>rL2hV?R$T52H|NWhkL4lsVh_eZs>ay)+3?*j_3k$zOXcVa_Su0vb)zAfY&7C0R zGVb{+uvfuKA~uz{BE5DFo&hfs(2VfL|Rzy_W1k@?uxbUs$M^|S3rcBhN$4YSnc2z)wWo}bE?N<4u=N_{6xl;&A6Vh zH6Et;X>x1S%}{N9n|~^Ub1a6`jxY`Rowf+BRqoo9GgB6^NS|lOAM#5%<|_Q1-O zxw1G`u=UZ=Fc8bqNs1)l1du#0hv6^h%8&)}?X0DoW#iNCs^Vf`Hs`QZt@BV!H zWG3?=>%UIsMU!fEtkJgUHVUadqy{|a8WgxJpVkIPDHcU;>K!LuZdtWqX|KgmAqxIT{t5E_CQoy+rHuJcqi+zO0$j;ia2Wj305R7HoR{g_{%uM2;qK? ze#o*^UDFJ=)8TrMI%5&}AF&kkVw*{`rX`n|O&1#{0c*c%fvbkQT5@g-(Y*#;k&Blne^ zs0Ojk(I$og=H~(;zr24lf&GWN!++FTyM3*c83cu8#bx0tQ|Fys<%`R!ArZPHB;=@g zkzt;ncdUJJl&*@L`0Pg)(L9Qyf8q4uFyD_=q)IAY;3T~ z8_v6T2CFyLEAld3cP*RO=&m10?R|h1^DqNUphloN^Y4;#7)|%D3VOZ=na}f(N$&+) zrJT;q?%~(HH!ho^(-dc~kdhe)f5S*XpAFrnEv&I}FzZJmQ+tvv%0m?2M|k(lZ2kj! zm}+5-+IwZal)x+v%+gnbr&?VuMeg znD9=@PMryy7-L#<;9TN$P_3X#Z2XBAT)*;HRV$_@^KpE4L!@Knl#$F&l+lRZAtEgH z*EPs+kr_681t3l~E)^H{yX00S`G_nI+0k9C6gSJ=F@57?N~Ph9Da8BIGlZt#wH3pP zfvY2n5pjd0cA&`B`OQ7CHe8t_o@=0Of<^d0vfvhDiFJPc$neapGiTaMKe{01pONpr|czb2e z?3NoD(KMm1hPD~C8ODTg(%DsYewR!k{*bSkSbC}$RjA^`kUCxQ_>$`YL^RDWyj?cb zODk)=B&`LTZ-w^TQDS%|s`0tplRKn{IVvTFGGUfllaqalkDVlgh#xkdYfwGEq>Vh) zxlVCM8OdlkR~iW9PWMO+K+LQNr7<2NT8lrSjD~98+s$^7Ql8^h^gjF4!VBF?*WMlo z?xh}aKvm5jD)x!4Cs>uNv&vmnbaYj3asAVcl6A74kS|fO#`TM!Y23b0WL$Scf~5oq z8^{F5+5Vey^XVz;=P==C3_VXD&DxIcmh)?4COpha2w0`CqAs!6-OD;+oh4A&h@)p# zA^)ZA6(A@0HnQ$6@&=w4WwyoR8&McauG;pJfsyuL6hnte97Y0C&sx{$wl2+Tr#_LB zHD&Y>c;I`hvH;Si3jNS}qCTieK5*uH`Lqlz8({lijHnK%O2HK!0JqVWvk_(;PJD%+ z^$N>n?q$?+R`=)W?5smO0D^Cx>bChm_&<@++Lz}Dfa$6cYqu@7iFC<(mV&&Bw zh_ak>BR(_xd1LV9W!-dVy#h#rmY!t8%`YqIL&|lj66%DQo&*uas5)x4oty6;DaBrc%qkc+YG2)<;IuiV6(%j_!F^MRKr8 zd+LWM;EoS1uNq{$Zcd&$p)mTyco+@=t`0-ZQ6hWe!{W|o;zL26szRqi=`MyJku-^1 zRFJ`-pm+ny$L)^GsDR~Nk#zr@panhPs`|YyPa4ZY22aZBHhx04pAgn=GXizESx}&F z(y(O>LCpoc;b*6uAkcpDhQrv9Fc|SBHE)N6Ua>VePY+AAF z+&>%?)tNk$9igD`kPsqGgPaOS+sB<~{C!x)viJHI{}}<0-P(H&IK!}p*5Pjpt#oGr z(a+Arnvd$wi=SHVYlCaNO|}xdrCeO{847;{@k+l;e}U_NiiX}Lu%AwT@OE_3x)i~Y zoB6GC$Z>?`$*NhUw=HdqFQZ|;>K-za8IAxGG%8$(yI!Fn{>Chc0M{bN%`G$B-0ihj z8elzJk6E6IaH*FV+rZC>j!FkBDO18tRa8;HFW34T7}n$4ikx#Gj3X`N{hcZK6Z1y- zMQU1i{B$)r>O^z_Qt!F*iKW$YTNuUjzEQSYRWuV@=1Z1-zdR~*J|cK{P8eqd65EKp z#Chj7$Tj`>S~|q2D|Ksc5wwk8&YM^_*0-3=X~8dhg}w~sW!8{LLYHN+WiPmWrqeX< zn(tm~$SLLLo-}HO3;0dR%YU9nDEP-;J~(-hQ+Kz{yqn#- z6dgsu7pD}LUHj$K``~2~E+9bHuJDgPk1)FGz0>l?`>O8~YQ5V}7^()ZY2U8QG`udH z`Mzg$GW~6kH?1wd`JR{!zaWxRzi!pACS97jHUUIGAr2caxP$P^Hly6zDAlXt2+6ES z(yBF{L#H1qzD(y*jtr_O)SZubKPP~+&F`hHnE<1ZM3={FCoM%+bfzv)Uj3a)c>{K* zj~RT$+aBK`9u#|uL8hYmqAXI6BBI#>9b@w(hXG6VL=jF-uBe@=C%!V>iWqLJ1}E@y z!8qE0^N`s(HKZz9v&$JCiiWt=YI9BIOtHNMp256{cR~%AAA{;bE!djyo$;I?WRxYg zL#2BDartTZ+5hu7nyO#^k@h-6$IC*)yt=wf)GE?ehR5TtUC0NgxHnbZEra5EyLL|!2$0|ew;)Y`;O-uz zL-3%DyEg6+g1cLAg1a^}8r&^7bmK0;UH{Y1dt}bk%rob$b7ns5Z@X$&?b`dk@3q!- z{Vu9)4U%@T{7G%lQc2@1q-gb~3jB`a5}%9zbV%@frhM1Y*EsobPpK`9bTezQDGC*c z7H&P8$F_WCdq0E2h~um7o3cl~gOXjc7xoLj)5c8OTx4l;r^+B6yuMl;?NP_NEL`R6 z_jz%Sr1uFeqM3lH%}zqO8tJM*)~7Uix6f%xvYPMWH&yy2N%VF_V&95nWi1Gch4|T) z$oM$3ZboWoezq+|SQMt(tKetedk#w88f*BTaw=ylb0d>GqYhZzKfcz}*j;?9M`g+U zF@zNzn_j{$ju?@>)@L!A(lwx(GCN1@urBTwQ6Y~Y#7rEDf)F9alZI|hzQb0wX)E?L z0k&(|7wJQ|nOoD}6#uf*`P0M^YiR7UYzXl z83TLTT~ShA!5e9mn))~K@t0et1eCL`tX-27IgEI+ku&XQCq+hoiuxSWiyb2z2*=dM zNUk(vv|Dpc&_vuyMmPL)g)2aA(AbTW<-xa&EwOGEBqJv=;RwePN;Gm(Ka$y$ZrMhM ztfEY1{v)6sVBj)Ld4)w_on>BxQ|HyS69({(%n?UDN>p_Duwkie%0oR!tEq10)K})sb3K%0%>G+eNv=ZG0lb+*`SKo_-~PLXpb&cis+INAH&u zog5tNM)u<{&s%7exJIJ;aT<(XzWE}KtI(%c{~#HrPfM0B>H2CX=`@`4@ZI3AWR@{R zfVF&O^#|}#d?M(Y_#WpRFc0}2K9-dZv|eZNUD|85g`BLXq$NG3$wx6!_EygfqLGq( zCqLV{jc5$7tfWq#t-UOYB#M{HUJO7GABH!pmhYs_4vBHC&sNHH^Lcx1>g0^3h$i^a z;(uTZ04|h-ql@z8LCdh|c926+V@zXlg=?x}wW8=oJm0-NXasqT+IQ*IMS&n)!gRAW zUpl$v%JATHBSP%fz&m?$WKX7u`5OoXn?w&Y+?p=;j)}y+7x(g$1=%7#*jwdCGh8X! zB|v2TiVQ^}z$nc;<|p^7#ulV2vE){q(T77f4+Gk1<=!Po(HxQUJO>NkMdNIuoPaBP z?+1D$^1#2OgQVBJ3$!!lGp@ou`rZQ@$G`Yy2qM=}R(Rq%cf-vkoU+XJ@_1t(*nWM; z{=G&e1MzV7J-X<7@rY#9duVHZipK3wnka4oG>W3#ju%J0u4_JBgr}Ix;M`M zJdl;ttkM_SKO;1|VGQRk+T;GVeA(q7!_tO8vf0=?->AUe@|;)eWT3dWM|=zJO*K~aitfi0K$P>*%W8h6#X)OQ zQmjDXS+0HJbJ39i2k|AN$Ik<>6Q;o8^_CG%v_vj6y0XobhcNR4ME~^Y%8*dIxAG8b zs?JCLl|zRmQ6FG$Po-7Vf@%0l9#d*3lUKExN1;QxKWVUXNk_Vjlok>z&p>J@CWvfI zXH*eFM~M(>ITNRvFWAzqs0fe#6l3qN%NvrolJF<=u<$Bdk?yP zn{tLA19j}r-IBR>MN9FZ_sTP1o*k&Iwy7=Y_oY2K+T+!c7LM22M9UoVra8b#fgGaQ z6IQdf2Iv}CWFT4r;51H4Bm7ee4rTI~uG$7;erDq#K zQM_~4=<@=Fse;%U!OgLJ9z&OR;!Ma%Id^4EIUxqeorv~7w8--pPZhC3eW>;jKfdVv zekMoY5I@t&7gWkS6V8kSdqC6t^}dKq!6@s-G>w&^QXmY-ke1FT^rLyPJ9JYXJ|QbV z`E!{C!-W0Td9u&12O6?&=@8?Rc(x{nw%@Qbmw{f*szcKzJ5 zTI)mZQ$wo|uFIbw$zaPPc@dI~k8COiS%o1AJaN^lmkuTG$fa2xf~Ip!A-C*52v2;d zS?cMBw?ADqpW}kr-7rL@rCcWS zX}DxSYcz5b_k4s>i=CYBBA1UV??SGF{8fmIoy>;5i60HGz;VGNOM=K$YIBUiXDR6v zDR?G$CJQ;XqaUrOwf=y;OoW5J-Ldj7)dgXKscDDqw$5An~(M+kp!hjQInkV>DT|A%#tgIi?FD^Mj5$-V13rJ8+kf zuw^qBhduQAmE}K3?~$olNLCm9esZ9_D!5;4&7$(G9D+Qi|D|t_WIdCn{rs22jC;>& z(7-*%DN;c{U|%!8$@na%%E?DGOBME9zN(G^Z@j{waeDexbg>5=!4YvQ9Z0czAS zQ{ZS)2}{{&BU=~7n0=fuR$oagP!bEomz|h()ntiw^fbaj`!myk{3)`n?0{eBYX#+& zZbOqNv@O!twC-Z5Dc3ZzNdEPIs7(H)H2L4ZaDdJKAZf0BxlH1zdfj8@g3oClT?Z>% z$44vcdJ6I8{SQ$luaEgg-LmVov zjy)!4N1rqQLOGkILru(Z!CI^(y&`-f#o>_~^Ndb3k}D=?<#U z!S7_SpZ38l^KXtG-|Ysbzh(HqD~lNDYx2d%dFOqY-}CX6kj!&mu4SW7c~y{J z{_CN|9#kWjiDe^=$vYXyqgCTuH&FvCV&z2ikJ@&9l@y0+$)U&mv|M-q?snK^3>MfX=owU zkQN^JU~_W!hJ8~ZL@IXX_>t;jg@bh94s}S z%T0<5Rr1_pEa|*{b|+RKcVg`fa}&l=8`YY*2C`PGsiM3U%1u7m9>$M$LR2YNWh}he z!bsey{TG0QT*Rc2n^3KS+(*1S*s)u1(*Wnr4jK0tc0TN;=WC zMpOsYgQ1D|Z_!`v>aU=E^_fq6l1=%Q@ljE2n(wCq=?t&BUDv%;u;60%;1K#cUefTm z6M+hOQ zwX^Rfn<1Sud-ainLjf6!QX(q@JOPvv3?DefEP(4yGc&pQ*}bL>af9v-l^+ZI^LhK4 z9+IIqrO-@5%;-vu7b(yiNfi6NQo6`V$hl}EG^}K%u)d|$D^;Sx$vF|!H=dhK%L$f4 zdfssaQk?g@B&!`5@qPeVL7a*YYM&FCzWG&4zzbUiCrE2Kg6gq_v-@6i?f)mKQ2RUQ zB3K*AGx*cH*74N9Q?QzoL*UyBxnwd9`Ny`QZb6hwy&bS&uO&keWOUKaxT%F-9X<+B zrnw9ReC9?qc{K`M>AEQ?1&`)1oM-(81mDM%7qsW=&}6D}>juTTe^w#bKEI^EX6EnH zt;&Ix150(66s<)^A<$PRpUWo(0eOo>vM$}M*A=jp>;mbHMV8$F)@s&aVl`b1k6lYC z^WNWo2}Sl!wjw+2H$>TH1~NoLo?y}Q{7L0+b%%+B`ddu7VTli~+zdptk1sCOCe2Xn@lur#2J=^D z==b;cA>UO~YyYIU>hjoH6A!WSbdU_wPu#I+OJsP@Xa=v z{dkr~sIxkD;_;ZVvtGmTN*jqFU55(ftfe(qF?t#J_V=8j5;F_A0c){%dYXsfs&hx( zd?oBoc4Bj6RH&io`5JPI3x7`IEDaw*_u`MX92Mxv^>w40Lv-ME{o?@$zFp^Ta9V#; zT(MHZ0b6TGe>9P@E4V(ugmb;pM*#4-W%PrcD?j@S&8UC*k_|qVMh?c}$#!F=1{Vmw z1OPDS%DTGGM2@wo{EgH2$Xry6FqPmfN`JpOwyo!=N4Xd2xnCCPeiEQ-7Ev2rKa$N+ z@L|>2&6$5-X72Yt@<<#=o_QmXA6anr;ev0(Aj8w`b%WYGse5@Y2moyV=@#ho*QpP2py=1w%TX)TvJ~S0c`!X^z6rE6x zKoz(niELk02pI%y%%8hwmnc{0tyCCd*oMo*jt_drrL<2rFUKr%95t06ypY$;^G5xI zJR`I=jbY{fZufy9uXY!`%(q)_Pk*R+G{}58xqYer2WhOgvm;G)7R=6uerXv$ zw7CCPulg|?*%Bms_9`d%d^O{YeVJ{-Il|1J(G ziL9!svfK9KES&vopEJL@u6G<|3f?Jzry6(EO23++-pb0>Qh%B8k-hM(Y0#CkzlDMq z5yBwStE%1lY5ONMbJuh0z=uP@2;0;)?mDecB6Xw}p=h^9N{_v(RI&5>U6J7T3bGe! z78a2jGG6rEg58*T$b+o0S^RSdAvGsk)XqHxIHoj)K^ywp%J&=@d~tg`iQ?8><;O6< zwWvL8?FYU+pXj%=W}wMjUFO>UL%JzAyIS-0g7pTY%-%E0*8D37mm$FJa-`-Xw8A=6>0d7Ka{u6pTn+*(SLtWNPL>3eLc#fa_b+fYFA~yAf zdS#{+iy#e$^YiaP_r-p75@v}8WeqHFz!1%OA{RC~r>B4Sxy^(2@6I`+nfz3XK%!Ln zZ$v5>M3?kZ1Ym=2f=7d2Cym#`i_g|>I-aj*IY=zh#-a7^C4Pel(Wlc1&`bpMD0aN| z2t+gd=Ywl#-?nR+v!yuUA8@f=j|UTR>1DR(X-9i9Q%$_Zi{6QA5+a-z(K0S=TyL7L zW-XYlAv*jA$!*`=mxzW=OdDOyeLGZCRItMDmvs}F9XnqQ1AMO`_K?}XE|(>AaL8;W zenoj@5IG?WtN6|}cqw>Jmp}D{85CX;`+;jETPJ{tVnUV=R~kiZ#c?h7y5>Gi`3QPX z0;_H)u*s+@n!M4tQt5py$zljY*OiIo{rqWFiQp+OD=25N0{^afxZT*L8U(XR&9!Db zrTkr3{(R(-eb0YA%vq2 z>-?YcX(F}NsJch z+ET*{uQ>jA0^|*K>>uWJK1|~bL90L;lbJ$4}8~8@CSYM zn>#uv%&6I2GCGG`kfh&g=}zHG>jiMZ{PmHVpV(|s>?vpRM}WgBR#`e)*+KW-o0?~D z_KTNl;)RE6M$lc-UxelP4zhA1j1k3t-RVDm`5l~PI)1R`pqeA3Z>#%<-It1WM*ZW4 zU0#eyplH2te(BOj`))>T?}^?Y68orMzoW@aVXeH>*xEWyjk0jG|9RsWl7{A5es2O{B69?g&~`WRQKdx)DqbTb|CTFi0ruamCJUFe9w->Rz^J`hO3 zcCv{}*=oVJ`zkQLW*Z&RwNU@gunh ziiuwyH_u9kmzHTa5jpzl6Dwm%+-p^aXD6*Z%)@}xh2!|40#{VUyw+M*a99E37Db>O zY3cPd!y~!cBO~Zf8**8DBQ7^`js*644F!h)Z@M};y_x#lp}QFI`K6~j-Y#fWnIEnQ zAAWNWB}Vzd_vLlsx=}orplQ~2^v1hM){^6^)Lz)Y@*<|?r2A8K&i8Y0B zGkN-h>K{vfHQ4gFr3cnrWhm2z69C+y5ilg4EdvrGoY|PYo(tSWc55Ovi=E|EccJ}d z9!Hn^^9P5ZDB8yi`ozV%U+W9vP2giS(0Fr&=@P>D;m%Bxl>V|Ukn>cZ{xpPWQ~VPvlCDkt{=g0kYlfCFN9!V@ zs)x7NIu~ZSbkRj=e#f1mo@F;}o?&ALSJ_uKl)$BK71L?3STfbD7B|kgNCMhQ=-#yY zgZ@FvVD3a{uqXI%2yFhyiP(#10i8H=Bz^1&C(J~Mscf1^Zs}0gEsJE(!r6~q=j#y! zIMr4zo(M~H1YdGv_joNRQS4Sp$VnfZzW#`y?9{)EPdiY}xh=7t68R0Cf#Mj;M}vYz zhPdN_TY~-cC1DV=GwlvPcGX!bZA&kwiu1HjPS=-t~#t$peCh`kpfp!UP;m zL`VJfDHb)7u~`0pSK_HzxJoz6#-n{zCN_8@KcTAG^TxnaFB6lB?>Gcy^%oUuPI37f8uBj*uD@Di)h;_9Ee@+ZG+Q&Oco+Q06NE(@8x zT6`zMS|sOhTBeMh2wQpXOxa$<;RuYD_j)%V-L%ZOe%|>p{5VgXt1&FvZH05?m|bhk zHppdH0mZp>FqGEx!F~x%BZT zaumziNZ8qJaAyF#fd?a0@W+J6dS{opPoyS2p&< zcDot>>jFOA#|3k#pTd$9mX1b_0u+fk^Dj+^t}Uh}zy-Tro|NF|}pFZnL~?&$Iftdx5Hok-+R`0-~W{VZK_(e&sZOF0tFW!8(E zm#=d9$I{#Lj!G8=yhx4M5yHrC3;+>HK7Nr49BS`(hpsw2;f=Z2HmMK1MdiP1Z=z#F z#(t^(Vy2;gxm)!kKyU^domT-(&5iR^!sG8Bi4f=@=z^T1x6|6(w^4h@zP!f%seV(-fTY~r!vDyA0LKHCnq`u z=5?$wC1BT9m8%%aTq8M_!6rHRV}(^=eXEDOg_))~G`<2C2S6oxta*ij;RhZ+b*s>9 z`JL$T$dWYq(GA%2e!FfN2hDo984zE%lN?Yp`tTGXcy<2fVgINow|>|rawHJMsQ-s^ z7*0pywwHrmOzEM@f4SP&#l0-EJu<*cGxN$R+BA<)R(q8xM^LrE)ub{S{ga%EWc9^q zuY1!Ad1tz-d_RQDt^MYqO@ZwwLVt4&;;<;OdMKd|MP?Ay)tY6kVHz>Ki&CAndl}{i z^@H_O<0>|tp$``RJgI7fZyeuaI}04m=lX9{abAxKqd$q4E-ot)E^42!0-y?E z+Q2A_gF+T~*S>_HYMe$>=llX?&95Q$i0jXDR4@FEvDPM>U+R{m>b?YThlPI}*vpaJ z#*tKloXZv&`-$%4FAnnd9J@Scg1=!E$!jo$px|Tw;m&c<*UVgp%Y*G`=B|!{{GO9< zm5u5k^~ww|Qm35=yn4DloBG@|fTQG&h^oD<^(N^?U446Pga)_0*V{;WxYf!>rxH!*8nSqlC+UV$Lvjut?@(T2v!-vi5U;JDS9DTKe7Xgg~ zM%d!ncH>*QxD!JLHyoO7EhD-;Aw?<@?maQ%fnI`>-h2Cb7`NW5zx*tkpjuRBpG;}o z83%^?Y==lUBtATQhbhg1Ar!A z3XgrzfdaSpQuSSGB&M9w1LeKPgJe`K~4?c%+l8DnahB z`E0yhm*iLIP*Q7a5v3d`&YtLWkvBB@J}4eI!L6#iX|J@q3$upJh2$AyelQ7^$ozeN zA?4*I*5=Z6MVKUm1e$GB=+NDu@9-RsL9t4ZN}y#N?#V~WGfV0^1ePP7@&6Yx;QvDw zj0|DjhH!-f`+Jz)A)I zs8R`+?co^W0#cWfAerIFJiFu1<@l(tEa_=~tS-@j)ZsG(j>`V!D2kvC~J+);@@4f|?BmuNmgkv(ezK>LiZ{_d*hZ=Ytw zdKId0SKMM$IIt-SpA~%fm;8s*tP9X4)G1kGTvx|(jv;YN?PWG4e8sC(_R^DLSArZK zFQg%af?(4j{jXevq8%yyv;17$R9B5r<3CT3ZB8^MMZL@^yk`m5mhU3F?%W?%McrW2 z_F}8#_JnFGb@DZM@lf(t^m?HvNzL0q-9}LP^?z>)S#a5`A*q~4z_*PKiL5a_Fj(_J z9f$h5jB2mrIHR3@U{BKt2#~BhLUZUFayJ3qjW2D_Wk#pMaF$LhhgxDZi*F%xwuIts zfS49EYBlVz+<$LO)^A^B2;J&)5FIEf>34SBg*MIE%WUU-4!K3O_(|$vzx?Zmur#LY zW^vS}!YK2x4a|XflH33uK$YL1_Y-Dx9zVEq$=#^q0u^mlp;Y36%=F$K7gU#iAEm6Q z;39P}mUApYv$NZP=Ln#d{#$yBw2k}49rnlwtMb=KThHD1Aic$O29pVG^z91}SgxRo zEns#iv*%&#Ewh4|hDoEspqNRP6r!(z7>r3yqQzp#lv(jVe71r8fX4hh=BQd${zPGk zLsn<@4^qIZ*lgRfmUH`#@&4n8))>2xxDm`;X_wObhQ-l?DtV^>p(%TKdI}V(Qc^0(8QGl2ur7(S z^H5DU77kecmHu|CWUNx~s*3&HDqP#PLUeX>_MWSfpB@9l>kDq8(Q8i=YzcrHGHn~W zuX>40xz4alHJ0oHB~#VHw^m|F)7(kR$Dz-QjP9WZjuO-k7Et`r=Te_$8BY@I=o%c)$z=k-cxB!oF=)2X2sFk+lPU_`U|KnqB#gK5U~EGWHbGPs`SQNtZGYG zqdDhNX%6?0H9tz-1>A5c4wLq+;WGos3b+fBkaTz9;^4!S>1S_NX_CIovf&Sx%o5UH zete7iQDTT6%w=>TRYw8+Vl#g9Z2Qdd(n%NE(U7b3J7?lToNaSenj#|Jy;gvZE=WbQ zCX(G~))!R1x-`5CU)CQ5I1|!2rvfObWZZs#I44RnUN10qDyuoIJW2EcW*kgrb@%z; zDaR9^WYw~eU*gw{x{EiO2LIbHDF0++v&TI0M4WXMkRrS8T>UHg{1BaY*5>!ej#*!Z zqJYh)T(N1z0q4%Lx}V!YxfWLix+J^ z%u%Ia^FYx9TjEU4Wz@OliX88Y5rOMc=064Jv2ONi7>4+Ii$w#u1Q?~@bWPi2&Xe@O z*Y`%z{CI6BmZZGh`u_&-B;iUiul2U1-*;+rRmT9}*PE(R6e@H^=`R%6I@B)h;&k@x z+OVl}3~0*GTN-A0M5JR$)K&k{LaOks3b^#i=jNzTa_tElk5#{H)m-scSh;N~a8zSAq>VbKb?No!6^tsd-ldt<1A|=P%hd zQ1K<`w2mCh#R#4D7czYycfPsAs}4r_7X!B4w?&lcNo9==f;_VZSytLqsq9Az0ZBAn*z|RA4qJ7 zx4e$6=;jYQ&n4F}u=&kuFFVhdzL`qg8Vxd6>K~XhQ*#fbn?RKi{d9cNQfOl!cy?1H zR%aF1Bam~kH`;2RFzpQg*6c-cO!ua_>xC*gwF#~0uM-~`?@7r`f<8mRQ|~m*NP-^)W2O{F$dK>} zY*YaXle;5Lp@|OqZSPB)rs+{Y{O55#d%{?EJlY?kVrp_9zKGWhS-ko8r<2g-l8q+_)|2*d5XJFb^Ati)88=NWg;bGPAb+kU34WqeeZK z6A?i!QDjm#`P@v~Ud8k6Ow4yvBS#*E)|ITq5zyLBSPbE0x zh3Q3amGH)ywR)b3KDW0bMyVdexD%=46g%f+BmXD&kf7D@kGtRqbK8GxS%EXkGnW4% zzR~=T#5Z{0f0N}+Xhr#IxK$hyr#3wVCmpcTwsX|p&iza@#(HC-_ItWsIhQYCyf4(>t)nczYDlym0wOiUi->+K3@+oB?sy1Nz z`R|sG@(wAxW5MBr*n@P)BmzkPT0oG@*1z30xg0FaEUn7zEQ5wj6S%PF(NN4BZCj}5 zAvp?V$L-P|Q%d#dlaxz8DeQj5$NU-)j|g??-VQMl1gDI6=HFn+^|w|XXXV&;^e@-T z(Wvn=nMihTt6aIiwBi(vu>L<|_WsTOGu^*Qo*5U0=zHK#TGK7fH!<#k z&2KfGL=gfE@w7v{NCl&2*mMtKEJ!%2;_Ug=ALiTNseRGEC0()q%6T+<>@ zp!%h2>@7zFB7k&@Q9$q24-OoX!bX2j|E?z`w#aKN^#SkiB|a|nznkq&m0@a=Dx6=$ z#zM`_k;+f>he*sN>6|9GY_e-2j?u!|b``-SL?A2bmQGd|2Pfx4F)v!r0uwGdF_Wid z{FZ%IHS9fV|7ND}b9M-kDs%pzs^oTx#|eK_hMc zU1C{8yD!Y%&M|38Pq*8i=lnSKkeh|UQ8<~KM#S|#B}^;@iBy1a-d2pJW$uZxM4COq zGU0fGQ}1AARFIoLYpmu^0a9g$HXf8@U5ne&!^zl-axVD-K1n*FfRSwefHzdGx!VBS z+-E(`dT_hC@kqG$Ai5yg{ud2#E#FbEFp zOGt5hS73^Fkyt=F`Gu!SLZ;N#y;Z4(GNn4R$S(tS4^1|_3WtvHF>|o=hf|xGxeiEa z)gy#K{vgy-W%j;H81uBfh<@>4nGJroABx8@A;h+9<++;EW#^X;(AJrrJk!>n+R>^ zROEx3<%V`6v{^RxD`&V2wYer7a?$R$HQbYhRk>81{~w^(iLcWTjJFA6^8Kk?gEmP& z$?!@Z+p^QNQdHMKGF&9WwC$ z-KY|N7w+F9U2>#gis_L$T{sgyv3NqMO$nFic^~UE;Dz+9iEJXK#Iqbr7wgkqH~5Ih z{rG!k`krDou%vzL_}LcP<&oaPJTTzqEOc0hZ*ErO~u>w z{TMQIO7cmaR1$ie_d~KdoY%Z*MFiB~9VZ@+@BBPraCy5U;dO9P9xKW$HV;!6Erf>T zYsIgV7HcQ_=?tmfW&D||u|I)i|znWNSfK`*f=XFDDaJ_U~a|x3${g1oy@*eA*DuPo#*yEXwIyhYphOFP>b0+ zEkTq(`2kLy8zJ7+B9>w6?U3M&o#y6P^lnkqdlEJ!Y*c4QKP*@EyVd#2z6_-bzmheC ze$f1L1#X)98cTJ3ICheul4#v@#Q_yZl#Ea`5$<8?yjC1V)Q!IKaC1zW_{E%Q zFVnIo)4{=rf&ct3gyiVut<*w~(A?G+gnHG2Jjmw7Z%k#=7y4eJT4^^Q!3CeGj4XkY~pWXkZ*k`|e z={!ScbM?veV_Q>Ir|d6WV@k!>{`~ZEd^Z9m?LHJliTtG-R|U}(m~mbFK1@(9wm@w^ ztxvIsxkw_pghn~B`HIx_Au8`bGilU3soGGeVvwt!r8*q>))F>m7JETtE@n#ou6!rU zH!l&5l=_M{1jKcu*2zko@F7B2xCdsBntJ98ix`hV4Sht+8g~C=6t-@B>xMFEl3p6eKDkoygcN z-E}Go{dFmdIvDjT?e!|v6FC-8i)*{JDKFAuQF?1SgSKFfNOHku4~HDpRj{9=?r6(V z+5ZtSdo2a)548|r7>&n8bw*`!MNV!wi;k)}fPtaTO+jj(Sc@7w9gvCq^0E_u5DV^! zHlx=6b+zsIqLVl?jPbU2)X9kgFVGw7fYUa&)`yp*)l1t^X25?uBw7m|ynol+`z6D(BJn6foU`<$kJ0j4vbd}`D_8}Xoe z=k4vx?1ppq;Y&WiTTt{VRdzlVPB&w3-8AHrPI^)fXntI`Xl3-ZH9|4ZiboZ$1A|9$dvea;e`+z1Ria?hEY^NYC9%T|>eiSEG+lPi_y)R=)KbKOi zrnv}zjBsGYFJH(}I2(92R)edH7}Q%qQA7|lwb}2}=O;$a$Rt7 zeL2O}>FSqp@5p*PEmT1w8FS-!jpP&~IY&k7gH?i-*>*vv3TZd9K+;4Sf`xclZ4PJP z>UtTu0E&6N{J!nGMo5+J!~(EmrjePcI~^YJZ;s`wIshwln9!I3=m5oLu2& z5gMoRv3j0dt5`-|=5QTi;R}G`s!0-bM*wO4@!RFyhdv^hso}UulwHDo^qO^et^Tjy z`}Rw)k;MT=m+6B4d58yO_k1Z4EyndZdJ0J5AS|$k$!SUM<0Xh_dzv;WOrJjNQ>w=C zhg@3`;oqDm@eCtYVLJQ_ZM9ZxH6lMf-=C?uo+j0co33M}V zkR%)Dwl4B^RCqq<4l#kWUR|ehaA+WO4#S}prI;P~klCeb!7rm;TLD*?YGrVCU zp%9+6#UL=Q)QKRlq5B)=KE2lGS7@87CL<@1Wek9<@G7e(W zgmvM5b5l>qt)<53iTt`sEvK9v6z5fh7pjV8TskW-7nsczP-hP>5r(E_>11XlL z+efG;;&Y`ddHaqB4Duj4LC+r8AqwoT4;RSBV`7$?8bK8S$X~!44ovmP%vetXD=(EB zamLyiso8vKp7tkHtFi+w4lWds@IwCNhq9lK!*K0)0#&Sl;*ofEq7^yJs3uNH~y@P2S9+pW*)YSx+C7cE=2 zHs!MJR5k#Pw)Mg(DJ4I67Q99`O^CkQ)(pJ>mQRi$NS?TI^Te^Kzl%`_ryKO=+cn+oC6L+iiQVR{g31VICU1aqJ)p z*kY{rvgYFHCizimg4ND6)Xf}}aLAOFQOISb+3J=r^m2UyPX9r^Ey750SY8l)Yf(j> zO9)c7bpm*s%9X7w6=mI`?zzekFpdmDaQkR4v}2no&RTJ*B5Iz{5M)Hf!lWfzG?lh@ zYD=z77u_rp(jWhx_Uu`GsoL~^JG>EYKY#2-*b>k750Y5#ND#d=ZXz-L5CHii_By@X z#vf*DB7!&*r$*S{HgsAucqj>76@UD+eVVTNV*zt-Q(fi@PYQ z+W_1qnyII+s|yEVL|AqPGJAZ|W@Mx?ihfInwTmR{l=BKG!cYvNlmdT6)@?}hi*U`8} z^E^A7X>$yWWUD1n&^#8yb5m-Fgb6W0@3J_E-pe3Fa8Ps+wG^dT`ud94LSJgrZ|aWZ z!qx_D8LW=qh?2$5O_K$_Fsj~+88cWzfq9a?fwx3??cmDz)MiytHM9oTQZK-m&4}7AGsLaY@t)5AVS1 z_gsI3E`QGCc(rcw)fF{;vx=-&eu@Qb?|M(?r~O=n8NIe20r^1fa`_?C9h6;!L&_T~ zqFjLyQN+)<>dScS zBa(2j6)WVVc;w@6xbTD(3_;`ucCMdTsw&0GMt)1=4wJ)&YRWwyw;hMLyvLKKu88#uUM%SC;vfu%^-TI@%Q@qOP3TY_%#a`khUyF2bduBTM~uiQy!rp)ojqi zzi{M4PjV#(O_~`z8Pek{9-8*dqaxoyLp|2S^vJaa1lTxs*Sth=1u#SF+gn)7Rch*u zEJqKyzu=k|wGol(tOs56GnStgsB?Zh;F>8_-GMj>7qOGZaLKEbBO&2C3cDU?Z2oQz z9NC;^;_G%+8OfAf{+8H&O)`8Sr^)9=MjUAGwzMP9>n@*h^mjgu9aD8skQ%QqiEb|; zMaG#+XxlB!KlGxroO{G?>~i^r^TJu|;an#}F7i6&-J7VdEwS{# z=<~FuSlO5%r1}OX=6EE;K&*=I1HlolPEn;+rg7|B`#Il2fmro4y)BWPXE{5Sn?DL; zqtP@eD2SX>^510v7R|0uB^GrHu=Jrsu8{aIhyW zU9eV)8z^Fr=qu|76MhI9I|yf&h){OzC{r~7&I*ZF`4?F1cNDP1l`_=ErWu9;6(jRT zX_*A+P6ZIV>-A8YAvBT+7Y$IaF3(#RC%2lfO-_c5(JnG30505DmmGjDC1ok)ugE#Xa_7-} zs%vZ|Mg8vJ?O5ey83%kC8RI@>i~;VpQWfvZ;mT|fC`m1GgC>w!me?jl8nY^y6E52hBm6J@pvq2>dRv*W727F1Xl#F0X|6GV$l7NR4 zO%)uamGcVHG_}*JXusVYi<&m{NcD=Dz&WAQU$MS)Lg;3YH|#&Sr(cPP7B~wr2;wo} zpY`NEdgM8`H6i%U0+T$$BC3^&@8#%sRi_fdlCvskW#yDl27=D6KZeb(bmD?4Qh2I_ z^bNHdXO5TD6w)<$E%#eQm20cxJssqK*G_B6HFvbwR=|H2xA=UE=_z!AqFXFMz^2Xh zXG-2$%+_wAG28r_{|9St71q|fx9dWI;!=uRDemqBE5(`u#oe6{+@&}ax8e}oU4y$8 zD-OZEXn`U{yYrv(TWjyN*R-|wcaVczS2*Cx$au&5{+{Q)6C2g~wssPfS$cNKmi$SN zai%MqexF{m%j#9=WefPU zkgYTA?&0g7aZhTon2lhJ;~)qfkcx1Iv+-AqO>ur|3+vl3)+;j(64z_+O2sJm6hchP zq7o`k{9U7ys@Bx)lQwYUL}I++K$b7-VIhD5)7m!);tr1es{6Y{qaL!aC)f04jR)|h z`O^tjI_z7fKpv?O3*vrgSNB5qE-o0uyDRHnyve6{jArB}se|o!Ix>+*f@N){gDfK_ z+~||w1lVa;5%B3-ZD_hFa&~LxxI>%ldvo(w5Oj*u>(ikkf3edO2zYQHhOH(*kXx7$ zDYmKTUw=3JERStxV!|B}H2^_uXxQ6%&=oR(tc=tT{6+;aj|c&%WF zV@?ze${4+ScPx}6tG{VMSVYV}p;dJ2IL%;NF+t@8nl{M(>ZmP2-%26!^O0<b63$SxlIu1j({T9i6Kd^yS+SKl++zbg-} z+zMvSiWguS^2QgY4IUn)W%kZY`c0fYC!HYj zCnB0dsm0d3zXk3eRN6Wt&&W&7u(9(;@OZvC^0Mqw<|azgB9V?(DoU%g&2z{MUIpni zvycj~Gz4&A;=SCzHcRx$$W&VR=~`<4w$b{dps4RsNpq~ZX=^X+NRpNj*=4lAP!Cm2 zbGWa0QM0*jq6{FCeBGETu&}-2YC-IdG}=PQXl9H;TP#0Cu?CXbjxk*MNQAe>(pNfN zKtjti6my$w!P=?5yZZu_ac)5o*NHt&YR=UU1z=Hf_({*k61ORTiU8N^;Oj~v9f!Ni z^DsZT4HZFi?83H(R?~xdkivqTD{MM7OmN91@X~(emvevVPFBt!3_d6lR8_F$FaeJN z+_~sNPFH^MtM+#Bps~{#1mW;p8*J z17S?%YniT>v-@5q>C&${b=KlONg)SukG!n3&ah1K*veM=rP-1)Uv&na5jF{*aHh2Q z7Q$9B`Q2H*$53Q*-&VJCzlP1hj|TcRlQxzqk!eT&0OQMx8afCn-G0$b)04Dpq;;k4 z##s9F8e?OmQ!8&%K!P5!Js?h(u<|FoNhodE@r(}gGo9=MUbDMYh-Y@vj2Lto(j8Cw zZnO_^FI4nzrCFTMwPhIS>nl=S-rTs4F@-P>9mb`RasAblzB&M)*#RtSY^rB;H;`EM z$#Vp(&C(at`h~1i)&E-X#Ev?|-_EgL-8KZkhGHvYXqNjJSe91-YwhRGa_fh~O3bcs z3ORf`k}gA__TPs$mO-618#YYT|zoc$sz%$L{a4qicV7Yv#E5!d|a2%@;Py z^P|&6%I9N{gOE+aDR~rzg;AsJZOBl=;m4C{|FKJZ=lSVZGVHPJH)D-)%8@=`&_vP`gZt7)Gxj|QuzKF$uTJyF z3!(&Y!P3$Z_wUc5KogcB@HJC)Zfn}9@LIY+{YCb4vM}visX@mp#H2v=%>6d}6}4%% z2`WXk?VJShVC+I-L1O&jx3OC(PY9f?a%qBzsEDLeY$*DQJpHm4$<5Z|>7dhv#taxEtqiS!tGUZU_Qz}bB z+mv0@o}V!%YfArQ z>RER63}h7{wlx>MgZtb zY67W__6PbMQ`~QvOXaYlUf(5B)B1eW7|{|98fu{}{du6Sc7&SyJM)8xa)_HX?euHK zCPmeCQLphtzSJ*eT=Aug6`{DF_rZn>CG39H*xtkXO;~RQK5Vfrf=X4%OP1KUVkK~QQ;lbV)yL)-tr?d zNTz>rb(G(mRFSv8v_@j&e`(o8E8LNf?BevHAXT1|zmEZBsz0vLZ zb)?JQ(dL63V9=K+{A$SDdQF-M-?&z_&_aszj0t(z!4w%gSNDgT2Gr|&xlqm!HaET4TJaKT@I7J-SC zT0N4xFSJ$8>ombY===xknV)giJo(MPw?7{xQV@Ug#z&p+x|oDQA!lx33#5$64b4Jx z4HQ^eh^Hzu|J&{@Kk|PCmQ_#QN(OjRVg7DRGA5+qH8ARRqDUg5P1Ey8rpY=W*Qgbm z#MB~{;X3lR|4(`-SYzyr@(VIC6V0*lbs0DG^^snBrfRNFcs{ zUxMnxi*$rOB23|0?oFWi>W4^kfnUk6KYfMd+gKYqb@DN!?C8RCIF`*PI8cUCd%Yz0 zw~Op|H!%)B;+z>(Fb9gHRGp2BBmjR#-H zY&-y3b-mP=(Iz()qX)>?Umt=5=%_k};f%+L`gM*BqtJa8pB`kzdqPKZ!|RwTNPR!^+@cY`hsY``bs^ z&u~-Q)Ap%)vlhQN!2`tK4Wsx*>|By@Nr>hW@T3lTDdYHmbPM{_EF-3~V%vpFj9c1U zXhYH!{vbBdSy@BVS1&oiJ!*L;czPE>?KH{yVWTgDKr5qFMCQ3T-nK!vsWY@Ol@o-& zV}oM(u5hn*oPx4bNch)r9Q4S&rcD3y4XX)ug~DY__gn2nq?$3(1IpH8b(`+v^XD#; zrIQm^G>(o}VryNoV{|rbaiz9+`fPT&S!ji_m$yza)g90aDiqEyKtsGj#q0-7&A-m1Yuc&X>LUyLB}#(Pk9A6;ZGQ6$6(msOrJ&T-~8G%EA>L=Z70 zQQKPzAu$Gy{#5qy{eyt7*iB?pyMzrtq;K%Acd!b~I96Ug7=1gU)->w2+9G7?=lx2{ z7w6dx(r)oG;$?*!d+d*G2cc1>*Y7`|#4OSxK#?Rk_06^DdMXG3c#gK9e-^ncY)hW^ zjopxq{N~4~Fw1X1CLq}Mcr!?)pmq|Nf&$Lv5)G3TOp12KaZ#|^I;8{rGv!3-%s<)?EO?9;C>S1g)mAbXW?nARL$kR)MHy1@;e zZ(3EL~CpCWDT6I<*`oHcz#o1=H>6S^`l3V<*i<8a~@E~f?nz5avHZo z4*}9)diU><$5ZL*hZausA&X^H;H?Cjgj?L?EN+$9ju3}$IeG~*!zQTV;NtVpwL!>N zpd^D!nUS<3O#(rD*T>Rz!!z!tMdX|gX9C^<@)`hc{u=DJJ8~0HGv;R=F;?>l2i+Zy zISX&RV^2;_G?rOZerjr-mKg28I{BAL7ijKL^JbE@YfYv-33Zb;<-Uw`HX1>S<0<0_a|vwEXG|PJ(50UNLAkmq?0W^y9m)F|isb^2|G~)+gBh+Ki;F57QZEZ!94tStx!QjhWs_7Y zIvq)(KliBskDh?i8{ol{wf#Kn23Y#ZsrVeAqSQPlbzEJvP-`g-OFPkXSj|1zwvADRBOss(*o!fTeW8 zUaZE7bDddt!f3H)+(3{XIeC;ap8S1sW5HOQVoR%@^(SWXWO^BHsMIBX%M|8DR#Lx6 zqpsFX@Csu*txo#;i*!{1Aua1*2Mu8oQ|^&#f`;E#DoqeHd&RFjlPm8XN+rp7qql}0 zhv6}dVSrotV&>!@gfiCmZi3I$O`X4^URHQ*dsSCrH7Ghn*e6#)nVDiju?pP3`{sM1 zq8913Tokqau261iTGT~kNN2CQUMp)u-%ntQiHJ|vXEvwKN{B@mLZz?r)I5;8cztMO zTc36}MdAF^jDl)2WOP77d$Oy3?;pfZ-uY=+A7}%9j-vne!GYR5K|?<#rW=HnhaBX7 zsr(`NyaN0O0r5H1%~HIIt>GwX)Tzwh_QQaE#~Q6y2aXFh_UQ;EYwr2aFZSR^IDwsc zTj$E1SBAZnXOfl$Ip{r=wkal&+AJzX#=iZjd3~FYR|bQt5CdtVI>0LIo%-|3Cf6EN zP{#b1j@c@Fc}zUYGt)RB>epe$j+h+EQZ3%Ix)pDY)N*St__J#Q3u^0gl!BBSCMegc zHdc5=>+D1-twzlnOHq2LOHUB1ST`t^SAQhg?5KH!o)$)!TyDI%`1vCL>TsW}WEKF) zT_l3wFF+O)2J-{oq=4g4960ndlOIey?~U;$DF=KmS#0`u$c0x-#|??=`Y_5D(Vk`Wv`so!iZW07P{j zi@z;Q#y}lkPg7^T65NmADIQiTg$xyZ)$2e$#uxpHR)%d74)a4*`yi;S*n8r(;N4m2 zeN*Fx`-Lv(=mr7I5B#!=FF80@4^1hA>j7IO#Ey$OS zydQi?tLax$E?1;6Wxt$VuD?qN7zm{@6EYYGzB~X>3GuNf2X|jDT^XT?JuPIrl-US{ zZG&~a026@KLAwK6Fh1H=LbPP!yAiV>-=nbXx4DSQDIX7 zBD3hZpqOy1oDl0%cHtA~2H%fVcH&l*>?W*L5h}@PnE7de5b)$r4NXw17e4UEMk%y2l@WUM)nDq z8jl%0S!QVY_HX#d_o!+$7h2JpAWyha$PJS(Of)oQntlD6ql$nTQ7&yrs0Y>N6+A2C zC^UGnQ0bQK4IH`Np9dGEC{Raa*uhvSPH1EhynPE}#gkA5g_INSrQK;h?R# zh4_;0GGYKkZOoH-YceE=Aauk^EOY)`vW>nQt^n16aADdf(;~4EI1gZ@P$RC!vn-!m zwxl}ije)V)$!Ochyf9>u@Cm%4w>dhN%(KEB`$B%eq@5RW!Y zu=nFd*6H2o6*Z!+0n}gO(m%IZlkwku`MAOSqkM{v(3`A#S7JOMLE^v}f@MrHX^``~ zI@AF9AsOwtj@K@f*`e!jZmN(&_6z}tmSUpO%6X`AH;?pfXB<+eqF_I-LNPU1D9V-j^JT};-_!U-QeXHwMF;=c9?x1-=+ZtF`=Y)6{&~HJf@4J4&8Zz5R zeyVXmr0_}}O)JIc%ci74U(5^_sgMxqXzvvqt6pV^PDnqyr^&;U>inV_r4ncv#o> z{(TC*Ehl?}-Y6;ba`ojKt3VBJN5Ej}%3sS9>dP2fsj>+V#^qThgPMRhu0_q2W~dZf zv3>;CWz`+5l{$wxFk*q}LaL80e-MVpEgqpmi%aD0vn^OVq&rpUhewPT`bYe6uVDIg z<@`%-BzxUqe&Xn-d)$Y0%ybpZ`b@nVtW=xto_L@T_OOjX7*!Qfx0e&0TCm@ZPesJr zSoWh2atACUhzNT)TeFbeD*1hAx|Y0a{X`^onqWA0(}Qr|VJq8Rl&?KiwmWt~vE81o z*)95vnt5H8%bCk%z1o%vb@8ggq0wl05KF3$NYF96uzAw)(IgNV5?;*=Eq7?CI%{mF zJr18i~$VM_uVI6`NLhX}=mw3KkHKP;6$w zq5o1ITN6qnV+YdYY?|ALg<;#mp9b_9Hb({o$0awDRBxW!`3%nBOrQhcmIDz%TlXA) z40wA^Uy2L}fU+>exU@S#Std78hJFQ4Dtl)?s?+~L2uYj*UhuWaG~UO-?Bs1`Xo)OV z%j?Qm+r2695QoKBlY;v*qHm3?1}k~)RK|(+Z%91YB1zFWHYtK|P-y{k->sPzM22Ap zkQJ4gMITsG3yWj|Ep^vp7|K1EHnwr&`Zv0lmw%7IRi~Vs0k3jv+k8Qg4?D-@IE;kO z%8Tw-Gv2#XnD6wYGumOSZEV$cduo0C?1@96pXh@)i9Oiq%kmudvMQGzweOQBiBP2v zCGV-X-_uF`9hc5fiKvG|LhLJ4+8Rc&pqwJZs6MUkrBv_im3{{bq6!xZS!uXFH&O|D~WWcw5gv3-}in`}_=wPgRIu$f<$G*E+bi9tF%IX0i z8flW5HKLvLt7hA8H3JAJxpt<8bEi3d%j=YVUHg`~w;wUiJ94rAKJ+Yyey@DxTS9@T zEXVe*pc*z`xRC>a@$8E9P|c4+qp2=a#u^(_g=dsBP@@1EyEpbBl1Ny<&QxItH!bJm zuV}LFD$jSf^6gDf1#k`3;oHj@*gi8IC(X;1I!d@u3Oia5*^N-sl#8hfH1YCSHUnm0 zinrhlY=TT4F-wDKy~E#qqd@w8HL{(%a5Z%jqpK&yl)*(LZ-GQ*Zl9M>x5Co1(?tE> znH&2b>`kWZiqD1*b1J0L^$9qb0fto1 zMA@9dyM!ud>p;ZD$8YKsmpzGM*!(^}K30LHuA@lE)eLz0JWb}<-=kQy;Wt`MC}5h? z?E0n+i5~FRnC%?0BVXKUeV5@Qs!N%@MgntL)4U4qc#EQKp3HhUMX@;Rn*HKvNB8CB z518(f*O(J~sSd5b2!G#DJ>Due5*zBoBzFd61uo3;aGkZSAj-jzoL&_Q6HJma&-G2)NEU>{w{}s^de|yU}7K6rr8c! z1V!_i@?kqj?lK796$c-xoKS;1q+(hjdaUw3VBS@59dqqH$B52ofH=U zqD2yM#nX*CHNJUne9mv1rFKym&r&ksT+&V}s zGWw0Kb=54ZT75X4FpUDiX_6j9aPQyafBxZUddQ%uVNFzfkWFCsF=2u!VKMfEQ0g(% z!N!pNd3<`O?twUS@8{j)s+H^a(b+dw!*9;nZVq0af)fuHhIA~UsJn^QA@-{tpT<%5 zL00e1S4D=>ot*8fsXQ}nb(U7x2gbRx_prXK_*h7rW$i@IxX+_kv=bLyR5dQLe!iFX zx9Llgao}ounkkGv*pIW+NgcPt`zB)UG_Ll$ZAOo^F9XO>;A4Z`2$el%>e7i0syp2g z|1?7PYnBqn=f{m^8+PYSo`^D=et4M$qXM{10+`P)}S0Ej)ttH6V z(WRaypoB3lZPQ6xu6K9CMKp<1bud2S@KQblfn%`()SmR%yNE+ zr)5pWp3T^-ni)5X3i1mc(1gucaA_4q+1r(P$4WH>m+0Fe=AYXutZM>d5}Gp>kOiUb zm0g1`Spc7*(?JsBaO!BQzzI>g8GwY-c2qlgS?c)~c5#+bQww6*y-i}_qZvv-Xr9J#B zV6as_=P@i!K1BVu7QJgMUD|m_c6P!P*FDda?qr=;&rt4-f5ZE(-iA%dbgMg)M7N(T$ zFL59?rX*I#uY-rzCPAzQ({$f|E}+xvsJ_P^SX$a6F?Wc59C2EDlA;b$KEB31zT#!d zk3{%aFV=$8fpftywU0nxda-06V`r&;zm5eFVnZ;W=EwznNs8}=f)Rucd`=* zP=o=#!$^@67ep0SRY!@oM9pft%Fd_kJw6F64RxbT0?aB|=RiN({i>}@AUW$HTa#d0 z&c|%TBGw`q{R-rS&mC)7LwCijIJn)FbgrC%!FJt)$k!~wp3jr%p8-d3~ zwep61+ScHF>dPO5Ey|Weqxz>3-*|DrMB3%P4)^!#o8rP+(Cq0D}AMKqe zhXi*T$BUojPy&-6iXd(XD!<&o^BZN{HQh?$3LliU?i`9pGdk??!Z++|B4s~@&jIS* z;&9b;$;O7Q;Eb-`GD$xg`0>DRH!wg)PGvQg_lo0I6H{k(C44yL($@VxtStYj*hh4~ zqglH;TE4y6w^E?a&W;3r>8uKsz8?DJ-^Wk&b*j9_BvQUWKsP;%Skv^cQW0*@-#eW- z#hxrvpTdBti&dNTpp>A6ZE1pcin;ic_1~fk_Kn&ou6K&fW&uQPVZrprMwvc=SifWl zg7A{qBojZ~t()O~js7w+a@|@jN!WjK2f+8Tz)) zy+y&YrGDvxPleQ_&H&1+vM79WJ+jc)?kA+9XyV2`Uv=7@Lq35JF+>?3l$Yivy*jhx zpspXRKck8{Mjpd(WFX=-vn@1uSvlPnB%JgO))QtgfHVscUTXNAOh-)Rdav)yYuGs! zl4@CWQ56dGw)^^>x1?Rg?i7-`$oti6*e`SbKsJ(PNon-21wzs)R9n$i_`?nK)5mlz-Ko-fFeX|986A&=i` z+4tb`XbOGeeq`z7UCXFc+*TGn?f#MWpqYshem~W|=Vc_J+V#?<-tC`Aaztm`<>&=+5{;g1?ZiTsBdBA+ekY;~!IYqWB~qXmQzm>h;dbojVXCxo zl zEh0T5y4KK$Zysm-sw8gzH%c*MxOfjtseZ9drs$+OC4ZZlA<=?5-7Jnof#6j*V=?^h zt=SoyTnMN9o7>>qsIbAp(r!^d=@j7)Gx6X#>KAXU1<0<4Tw=qCZ(i_{LrTs4B|K)8 zMMcwo`8G(N4NHYUI8GTF`;H(;!n3R!;OvsL!QaXlx1bltef8Dtm!>rNM5{I?v;`)+ zF0}zYpK7vP_p~Q>z>+TSb#ro`ET19wvGAm3`@EZoE=cRcg9S;aSU?eNzn2)$)Vvl| z1a80T7FcfT{Hj;AiR)9E8G#FRgb(2=q5D;X85KRh*Kirkew z!R}4tsguQ!>gh!ly4}f!k4CQ-AHTIfcl~OF$9x2!w@E99P|XN{=Cmx^HBp%S0>N_a zXla2D!{VbzfYrB*{{Z6<5199p|br9Z=)P7p-=~bG$P_{ybDC?aJ|)*X_fQI zo6JM^D`WqSw?#%Zh)Tumw<(+t(Mo zR2ak1=r2Q3W>X(s0fPjXRxZs8zV=;oaC$JNcs2#pX%)S?24W3%XJyjf(SCk^*Mfn* z(@ipQfl~$A#)Z%I@`47*9WrU!h6d`q-e6!NOpZ$qNm9F4+eeD5ifB(?ARRh;23Bz%K2c!lmQ@7HrSGVUDlj9Mc`JINh~^!N|`E+s;~K~h6J8Jxv4KUJKFM z7oz(m{rO@W^OwjO0=`oyg(KR|kAkA$s!7Pspu3ulAXjw~&4z)tlo|bK`!|#?!$EZ? zn+YV7+7D)VF&!%QZL!JadWSO%1;(|7(YK;`>86swyJ(~e7(5=3M=fC)(Qu%@fy5{| zG^0Al2LviST&O8p@++NSgYssEVfAo0mv*z1{vd#KWK?Wpe-KmC_NoewvMz{SUsbs6 zX!%Z)boOrdAxY<}>c0&h{T~ds|7&>N0qbP=8zUm4^MXEfD!A$@?P`N#Utl!n)vrGD z^K7LVp?*g}-m8M#N&BLv){MzB@(LX+wZ3inHSY?fcz=<|Hf2P{%ZV9S*Y!8~Or)#12Q`$o>)F-EGy0>H zUmncHUdW#FXL(?ZVwfnze2q(%TOX2~0%RQeZ&5VbXoQC^KA78hCf=R=_!bcXN&5!UIx=TrINdM?L3OP04;8xiK%}6 z^|KFfbUE_SBB}z@<_4C4g2~>O=8g1HGm(%`q9V;y>-{d!Zi*zy?SRE2zlvkVS>25Rk>vb2 zQcI8U6&-lT+-xlW=6uKapFZ;-c|bI-jHB+e9Is#`zfX)Hd~#hDGE~QlMvLD8x>YH} z?EGr%TbKYRk5WE&`PU@jnS#CEZ0Q{rx=kPUkXGy5UzKYtmt#VNs+OF`U%Nwh@@0=a1?mX zj!bNpxkny6NZbsAz6EP>ck(qkS#LGjN3?JLQD=iYC}6>>OTgMBhi1W_F@dy&x!Hh^<9)Wezwo~nc-ve zm)4OBs#@rM{phVQ3zTB7nP^l75mZtA?5tY`GC1XYjrBU#q`$Cl5J-f%tPbB;g;G1R z=i^-Dp%7+q><>*xLs$l@s?yK>#_wI7){cY+m$I;Lrmeyvgt5?vjD!s{)qRJRjo3Xl zv+*xi`csZUlnrw9Clb~cP!lJ{5$zFm`@gHeh5A>{@y}cYbtj&FbP)6Yd~oY&ZMh?G zh_~-|6nZ%A>Dez`bme2m1HsvRCwJEz*V;QpY-20(f4Np}JnM`R z@R}m4bAmcdqV?}7;hgNcP3YEL(CR4od#3!Jv(LBI)W(p;vhK%z_$tX88-j;;fIQd5 zQ{PZGw;4MI?3x*qB-7?(H{4)v*-9`}Ah+k9kE4D<`57*;(!C<+zhndx6(4ju@QeE_ z=!Ng)(|o%Y)B1BZgQyJN601qO0qt5YdDhCREN#mE3r*w(jRvyBZjEM?jiUR+7SNPYCk=0U}=vdVz&Ug{DtAH=VutbYXlZS#N zWH14>9#ZImA34k~um8ZqMe6k@O0;`XC=06i%|V-JI^cq7e_5!OwPV|iO4@7dy(E$* zk{ZzjHeBlUbB-Yw@&dHb#uOWKk;!Vjlsz89_YyBIw`ogWNb;xL3kee5!9q0GBB3rr z(Z-WPj()Y?iK}wn915;D)MPHOr6F|_VqdtcG8E$2G7Em#5S|~y!c$S}rVO)4Nh$=< z4DGMT$!kye;4jMns%{zxxesPrcIebioKBxS&Pio+dQhI z`B;8=BQA{^dDg$%Wp8MxGHGxAhCnv^~6m zy<=CL=!dru9G~_o;XNLh!4%<%qdcI1#IO9)s~5>ad^08jwZ>8Gb+^NJVxWhxu&-9- zM3F*?f-8L7pGYbh#C4gE_lY-jb;Ui}JPQYvFdgh7mOQ5=kl4ukSNRJEqjnb$)SV7KeJzUo&4ccqQL#Gbaul|lcdhlf$yxqRo_y7vsAXwxcI!@#% z3ZSKhl`&3Ov~sMb!acHxL}vi4O8sxKx3A>tEHvcV^CKiYi}X)qFk8xc?W{@w7?Y5j z?ITQUt0Y(Vy0koUI5IVTWT$2vPNuJ}A6k4j|7b*&^px1FfIM5C#*ir61}ibQtKMPL zk-XF4ZAU^yHq7aV`5fK8=D_+5dE+e=xRhd5o zzp?0xDY`i$DZ(j*xfS8`e_Qs};r>x#T%fK`YFq5xhD_3js1twl)Ktyd!0b37(Qo!6 zt4qQoQzVsb|83#ho`-wHsnO~QKg#T``U)42Euma8g^H4j?2EeRqM__&CUgWJmGK^1&ji5)}vLwT=qR?vgF{w5fphHT`Dxm!@E!= zS95%t3*d4x!+JM=M%xh0BRi18Hcsq`e6Ytp#XrCD+e`s;_cFN<+jbCcW<29S=}$00 zSP5H=Mxbb2zf*mEoK-aalkT@q*{Dg8K~tmPbaH2ilIqdh1ABM!XmgJFyP#;RqzQ(r z?vlf2GoaD&{STKJ(Cwv-|8oCHonErKKg}cwM6x4bvYfOP;ZjK+`Ik=9vaLJW?jHo~ zE3a!&zxS|S4}sB=*mnt%qX_TNH{S{m2?4g7+@`R8q-c5j6_Uy3Cv@dup?|)qjyD(> zM!x%h2q&8JyW3qRnX2OO^lgf_fr=9Hv4d6Bbjkw4yDlt9fgQrM9;Dw+DV|`%WlBds zz&cCt1;Z4B=|A)t{{Q%#*xxq^A*s^y)U#TjGWxdI^^iY|NFwZD`5~@yDWf(!8La&+ z=t}IgE&7oCf|AOJ_9VW}`~@`Ur@wOyW-*P41Bc}k-L{>OV zGbClcNIP?Y{i%W~%h(8y=cB);<+>!e%U-}j5IjX^=D?S7{{+`CcBFU6rz~6^7S9~% z;JcB9452D>mA@Uqks1r|*%2ma^ZSEP$_{@pp^a{y;pDh#uX?v;;>nJ{i`ytm_eq}g za?GBVt$^ArqsV|GlqmCMPPMdKjdstiz+X5|UoBKG{Go?u?O1{kp-CL?K;0WEhK9y- zK6fJc62{{Pu|7Mse_qZEH(hemo=P0H`GgrDrLu%14cjp)gTtW(8n!FST#pq+mFDOK z-YkFbbpl7rnj_fL%l8^f-nE@`Iq7tfc7-cm$Yy)E?u&X><9+L~LeJtJ{*1mI+e{2h zcN5D_9(>#tCxKjbv9ZaidT%p0EK$rt53!hdoeS|~D1tI!MFQ?%E`VjDL z7ZRKUm1tAv`E6)XclHlL-8HFerf_Ao_+0b4shk6#{5QF}d5CEt7?rY3WlqG_Yo2iJ zx6i<*L#S^18c6fkQ4&B_OmiE!S)YsQ>=0$wUw$r&GwID-MJYHOuMJZwD;)%}L0^A6 zv>=FjW!VxCrXL+xXF&^yhJ?(1IP0@YpD9S&)iNk7L$?3ae}NionCDZ|YgN3wDo|UX zm@#t|!%Ukb7^ZNb&L$87Lr2}WtDX&nbm`-e;3FR7{Z!3tRn(qK(wHxtW4|7vH-{!8 zzox@TkR1)v&_JLbHpn%DWX%h+bI3${Tr!?9Bya2R2)kiuo7Kn}O}3d;9}0SR1^PLq z*`2J>W`4Yc%Do6e_k7@is*AUORosn#L|MSM2(D3Y zeX#nu7G;ubCay+VB*}vnRFH*W(}~Bfgi+W6h0M5z9g$Kef4*{N)}*kC2UXAUXM%DN z2hGIm$*j#{jO*)Cw#)$kYmTsize;n#fNJVv44emI*{qWNPJ>0o>A6@OiQvuY!Bc#J zX!snl^o#$lV}~$fUE+lD-2}X4XweAt=mb{CU`VR$bH{Qa!rohzSbuvVHp?I-po& zi>zBj<&R%A;vs|Nk^Wj9X(9kMC?<E?6Hu)@!gkwMrOoZmAX>(iu3KzTf`bL0OQ;QU0yIOF&s;ymWRX_raKdbv0|LPH zci?ESqFfss;4Ny96ZVy@4{CjtJi6X;S9c`Ck$OcL>l}G@?eJP>YO`_nYWZpp3X(Si zk$zr2kfS+W|JEw7nS%0P0j1>a4MGFk`_R3rt~|PXpnMG$oA~>;-sK6GSxJdpN=h!$ zYkBO|E_|9BAvJ7Z{voo)AxzdNo7@b^qFOQ@RD{=ylogf112D3qg@&c|>J0ooomYg_ zCSk1UbSMQ5)Tl55e_T>bSQ?eThmVs#&zJ{e+9Ce?5^ECmPvt^a2pB=_r&fbE)9c4j zZ=%9!{k5XhWGDMX+3}IIuVHreSqF5@O3I83mDYw54-9e7$>kk_NeXNYc3)y9XIJ8f zZFfY841LUR0SWWL-+yJW?AP#ViR@k747>8Ij$FiZC$q`#KnY>CRBz88+t_z{nMA(A ze1ZFDX`&-X31g(-mcB2SyuGWA&bFF$EwI~4uiyXtO1ygW+Ov7E@fA^ARmY;hZJmM{ z3((yvk~+3hXHQ5|SpQ3pQH?bx0B9vKAb$;d@_10~UH53#`TTvum;-0L3`2H-#ot|l z+!;r*WbytabmkyO#o%-!s;+X)P$18Ik@kZp`&s4ZxxGT3m-?MUMxF=Q-w|2>SWX$S zC0-S4PZg>ypv<7i7`Chi1TI0UN~hJ`nd2v~xckR2`s9I>*M@~&(6INBa;xH+j)Sry zm#?~6g4(8VeuwRE^ufXsA@n(NW_+qENo?CZx$<;Udtuad7GhjZqZNNGT}^*2U22El z6>hu=+MC)REUnzx!!@Gs*xZv5T964T1Nhccq1U;o(jobs0=L5g9wKxw&$+cN9~&BN zU+D0oPfuy5h*uQz)f8hXKW9XTv;}CJb5~IRn7Y{QnY?4!`lr4o#wKHvZ!No zU7NhisBP0$Tx`rQCi(C-JhFPjWGp*`X``nOb6-`HVlti+bZ? zqniEZB*&8I4}#s(@27*z?ulin`yYfG?pSV!4fs*3grR;Bs;k&POqpKa6h{6XqA$x3 z{nL!x!A`*WMfA<^-<|`jx+V3FpWrPRLQ0K~2W5vMen&XM1j|6d(fGT#f%$(TOfE;*3|7?3gmZ@17CKm63Vugi z)imjR^gW0b{=D)cxN(Q`SHi0sXXc8{CtKi9bL={?1@u8B!G4R|OSC!lm!KUr75Ws3 z@DJOI3ya9#qX6lbsZJD=m!nRk9jH7a32n4{!KBD3q-#N_Mb^L_MNut5IqjgQd;>Zi zn$gb|kgcwndh>77@i@HmPM4cz)X2?RI39Gaww+~$(ifr77I+wwvB!qJT$f~CwM=D~4k?3|Q z6673z7>{m=7M?eXEW+%XN9iI>1mnXsWTbmITk2LnOA2Da7k(AEJ8rlrTPi-?e z>dSa(NC0L28l0`yPuROECiAgvFbhHn=D8WNp#w&6u$M&cQp>t&yqznse?tSofOIUx zmhI>?DZUUzH=j360Ph4QihX=a=tDr{A~=#H?KcDLG~teT8{PkWmW4OP{frcve4-va zcjT8fW%RzR?XV}dCur9P$D$)panEV$;{jcqD2OgP8svNN`BcYGln@Zy_+9=^(W}Uo z9=-Iv!QMMtg9eCfdaM7_+>*845zWs>TCLOntGV-xYI6JAbdV-Rnt*gsiVC6kuAsCC zq4yp^0wgpk0V5p&=?F*>L^=VahbFyBQ+f#yArPb&sRCzm&ivP`H6PwH?^-h-=JT`i zZD&9G*}v<)ZWO3k6}Fah^Atnx@s5L2oJ;*483lmgx@6@qO6Y9HrC9Z!n5AqNsYq+V zKgUZk+|YaLM5zV|r65nx2Z!}AQ}_g}|6+HmXE7EQs;E@_TFIikF^ht+r|%|W7?ebt zb7Kg9PH@gQ!Mpd#TJhu)b>+FQ(7kit}%;Ohc>o&K~oxqYG9QxsR zNm)h}c7E?Yw>DU;|MO9Iw-f%fMoZkX^{t(+9D9}cH;IYZi<5lxF!eb$b9zNn*umc^ zI!22l#yIJ%-V+>~qxmEu{AY5!`(+(kL(y= zjm!tV_fhwyCT%=THvVRP1Mu#;0VY1Lyv}mS_P;@+q27`MbiAkNN3v)_p$E^mTP(cD zMVwuB=--+;0hI_Jvo!uxp?q^*`sA6a1cC|@*sii0&$#~wOya|s!?*u?iGBJ(P3Fqd zPe|$yF4k2q)ojO9)|k6aotG#ACnS;Hlb9=ZY~=SJC{NFGbRgzNc8)S6q}S=}opJ5- zv6d8-<)exRO}=SZrGZrQS=@Ve#*$HgzP7sdF9cO<&-Xft?fR6i@y*1= zVYtvPPpGSJFbR8is=%K?mCbWf4V&ZAyD@d=_-VM5j=+lfvGL);_6&;Q8P{(QfHNkU z8j>8~yJxiHgA}HvvW9v=(FvZO_qAuJ0?IT_%4DhzrfNT`y#qdJSo8|A&=KrypUYJARdG0a z{q-+^N>=P{CaXN;`oroqX$~f7#^a5Kk|Kkpl@`fXjahbn>GqROrh1gW0bc9Jc0Gk~ zc_HY->uJqBZYQ;MaR8H5`rU`TAtA}MYUUOZ%L~T}sF57od47cNO9ekM*|9hEZwSir z*8Qv0>}d(AAx1HR!2+PJH!v0%o5#C0`$BZ5SGy&qZPm9eEFnV5K`2(t5<^uwz9az) zr0&op)XG^xK87EiR)4=RVk-(M+4#|S5iVAyxx?aI9Isc|h>5F?pJrt>43!vfxqD_d zeG!>o-0$t~=L+RQ!Ti@<*K-tJ#U>g&>;gnbZixH$D?(DIrLrt5lsRRzYSX4iA1(cC z{gWWC#cM~VMfnN9Xa^g!N||dH*>XSQZv97DfMo>!Zdp{9v*}yJA8M1Eox8P=>fI?F zqLU@But4vx=X(mMla`C{g2bt^HtaCvnk-AJ58^+%fN9PmE>-xp`pGj-nb#jK_)bto znwXQ5^D_PFpGL2qMU|G>E*9nbz)lK747W|!DR}5ZDKl=o$iJOTMoXL{0oI|IF%+OB z-DKD}MJJCyVNxHzEqwjji=!7+QQqG5Fbd7Sq3o6{&o z(^P>#fN@UR;YxVclq2i#2hToUjW+ryNkh4(;JsK);U0IY&@=WJQTu2bf6iv3#Ixxa zgfoSj*58Lbe+U6P483E**N$NBVo&=Aj6IfL7`rAvPc0mtQxrk<79ra}{ykjU7^0J? z%WnEU4viLsO+ZMXh20!A70t-~r%jLIqS7UlpCi;m&QH>f#sn z;EPodKV6Am&&Q=6Ng^F)^dguw8s2NtA4T>e0`y67w)k&_RD0QFl>?Voc!{U!d5H72eyyc9=2Q}6m9X1n|| z2*#tGM)g>jH7&4RYHlsO_hTm`sh$@ZCdUhTujYVP`oO&9@d{qIDtYP0`%vlx9`uI* z*gUUn-y27=gsr~?lKSMAJTfx!g*%n0KpD}w!L_fG6B_QUZdm&QJ2`IbOJ3^LN|BAF z8&mGri+snCC=zK6$W=PnHJVGOQ(ItH&vQ^k24LcwT5fI$^<#rq6b~U>frIkrckYvG z=xHoH>ZO=dpnv;1hOkfESpU(|PvUpS#3XWBH3NGbR_q zzG8z7WMSIkb?+ki)k!A>g2^<62K!ys1kNT8es&ikl;FK%SuIzgEuIOl@lvYsdEfty z_x*qI>~~heG3ctbM=G$!i2dL1Ge(zIkHKNWZ!qhD#N*@V%|Mgp?JSl*1cZy2}w&Og`V3Q zCm;5Rf7MveU48D*;wZ86JD(h&b^~DJ&fozivIkC8B5^bG_Xpo@Uu!kI8#p}rn4`Na z!ir}PIGV{cQ?xF(+`PREoZj=4N4OoCs|ioen>vuG()%X2m=be9{`n01atR=G-P_pi zYdC+OTXbfd$Cz~;Em*vZf};m`)$mWVJlLTuPMZZs)0Ol7(sHe`nW^g)FsS6dmQt%Y z2tZS22Jq9axzT%*9b!-5(*>`ct{6FfMx zV`*v-oH%>{Uc9>+GB8S}`w=2sTsl8%2|KDR(u80 zEL-w9^=AfO#Jn7R7*A=Zfjk`&T5&vUtvE9Iy>6%ivK{aSrC8OGJmlvs^x<(d3-Gy} zCJ09yyR(z+9cL}k;cHP|1#++W)*$mRlGSF2!5!M8U{3OZ#fki00@tMpM~=YMi>dpQ zPt$%_YqXpunyWEiEeHA=5gm>C5ohNb>JmmL?Ur{yihi!_+_CqcxwC1oXfQl9qGWpG z!5%Cgfag#C<$-j2#TFFy`yJ9ky?JU!xFUZd^F(&{XO4(WD3Lv#108)1i`Z&R-krB~ zi>fP+iYM8S2hxEXZKYvnn#JaKUJ*v#%3Fg%TG`lHssWnHD*48a?rV~ zCRhF$FFJEUlPVa%y+a=GCVkEsC)V?Wy|PjPd-Igtl=TE#gFx0DJ#UWW4R>@3a8yfCr9 z{m3ALhZp8R7Vt9G@Ym_Js#HQmdFZ?49W6pa>P?UE@cJ-fR{Vo1!p2FWH7JM$uT-{! zj+7L1uD9J0Qr$JuQ5GCB&#NVgTG9-5C^A03b|^$}q%>kpNOPm5K=cd?82*7zu0AoE z3{Rbh(gg;-sofd|57W>n5h4O$6o07{L8?ZN&k^B&mbN4lm%mD}R`Y&dI+ohb4#F6K zAoR2R?UW7b6knr{&VD?65UIdA<4Ql9NYQyRcriVddl_ZXeibgH@|$9d*_;iUV@k<7 zTYWRc==KH~fU7&EiKY;@aVNk%HMb6;!d+y5JCpP+ev}tEgW6pKrNNv;Lskr>- z*jf(Ctz!`)WIQMk1hW}Os)HK0mu5$3X?kKS2dz&cS@@D@dE!5L9;1*e)?fp%dQ4T_ zkLCekgXxEOAUSEFF-<=vWlRaWaev&|!g0F#L`lUs$IMR4v85?(nEPogGruS=-yu<- z@5!iKuk}kU^u*7a=Bn1f7KPclRfPRvfkC-(>U#;If$jbp%`bX*XjRU^9+eLr4m)T1O=;7|+erR(DJ9cdoa z`4`}jGyS^rwEaSd&wgs>uEF(Sn`^G%$fCQXf*;{($^5oPHum>owqLJ)(Wy-?iYD@s zZax9945m%z>tEp3YF!X(14dJxd+m+VjD29@Y+5PDoEd`A)d;dLlIf!6mdjwuxwuk} zHk-!|msLkZeE^FSKM zJH3l{zta~<`)yxj&Z|(#3~B0zN2+hReMz$I0t|Qm%|8t-*jwjRR_~F2ljVNkpu?pj z%^s(8Ea6lI;>{mY6UIcE-b$2zk?Y}-8(~+EwAlx()%}>qDAJ8NJ%~Q(+4ggsEG6>i zmLXUa!>$>2Z2tuQdFhjT^469R;$0@3j@scPEk~FBi+j)O4s?qhgFFLWfChmP3-_)^ z(SmV>n#PYJNEm3o6PPZ&Hg@IV0PD**6>5JWpDW2_%;KWu!c|@#RH(f5I9MQ3L`?Xz zWGD9M`|n;lfAJ-2@;~Y@2P&t`M-S>_UWO?47<9mIiFyp{1v7EHh!6NVFB2yOl>7lJ z*{HUaZ4M0fU5FaRHrWbmay&c_TFcO-t&&Py;%j#$uU>G5X$1*C0aNx?}o91M? z{yaGWM>^*eh)h#u#)+M6nM0udLm0ZP_e@^_~tInL$a>w*rmWn?^i2E@52ocrCa z;If6;##344DMP~pn!Iv#REbn;pRyck26$bd1xIKmGWe`s>dM!u0;$G`mS^Ud(j=s8$kHjm$^3{ZGL?k%W3{e6sx1 zPy)s5YWQAk^)QtM}Gbb){>H#aVgkd6&Pxy7E>gF8=sr@;-EHw%A zU)QkitTLXAFQe6+@)BX*60^1%g+nqbV=@d@<{v@${GY3D8vDkR0XndDhTpAkY1a9_ z`k)Ag0;@1f<_lOvn*H}a$GE3aYn>YB{rfo*V^SagHQw^P;lSyO>&e%eh2fcUi*-=e z!ocmLH47(ceu6WxxljozvhG}v#QCY`LJ@OGtAJSxW+$Hrt_a!S8s5=_}DdSscCoYnCwOY)idvQT%F(fQ3 zusC61orI*szT(h3^<|i?|NKj+!UhCmjBvC>@W-jbm-Ioftv=6bv_|mKEmc9#pFZm! zL%`&P0w>9*9(kujysq`NlS#97DOO7vr35ERxL@PhF!ZVKBk|0=)}=_4;*)pA|ArTc9J27~*mHg%o5c&2M8Cgpx*xZ92S zad(gSP+)mJ>z4NV_qHQ zrNL6uwOR}uv!p&+h(+lL>c(H-4L?FaQP`I7ZelQnkJH`zy0a?xNR+!Tf?aa%Ou%0w zz4M(-4SE?3!FXg5Zyan~F_|gTusztrtk^9nSzWhyW#pwFf| zCQYvqv)k=K+-X5puSM&80b9x#!%}xPn6q4SEGewrB|(BGtX+=eA`=-@IdptSP1lxu zw!c)1VJm46%Jj}g3kYHy4Y7r)R%;J69^X}#W9^s4*RuezEel0UQfp1m2vor9B9eIH zblw}Z91KdEi2AKxMVWfAJSbNBkjhmv&qUQreNs;%QLYcUC7*`ACR|rwX~A1-gt^dc z&5!LYHTj~7Afa**1wgtvX3@@0d`FM?yn`g@*1I&0ER4v zshwQm$3;6qLMparH%!qvMLX#5a-T=QW$kI+?={c0*qN8PDtPk$!eHYOyi3hS~=QPSr}o66{g=&Qu@9shmGQ zwzAK1cxce$VHW!vA7Xyto9|O-tAViRj`u%giXkJRd-4yt9D1{Jgb#n^tT$BO04la* zb*LJni;ZS&H}qL!Xi_pAxEQ-IZ!Ew#gIG)*KrMskE0+*X^Q)7*9g z6bR)vt<$^FD$v_`q-%y(@CRg*b)ZZygc?d$*^x?c?wb1R5v|@H6B`?SSnJ{#i|_fs zrW!{M5=9wA=W<7HuHOq*$Q(!lJn=~HiWR738v38FejUG+3X79Xg^Yp*^Zba4sh>5G zJZm)wN8KJ)c3IX9d57GfEyBbSea_a9L-|&}EgU+7PM1zGlR7;2y;o@kEV}>hW@03) zTP3In>}DbpRfj^}u}tfl-n7i4tUOI4x-FPUp_-RP>K!(wr{VJTlYiAO%lt{$n`OAr zt#Sh9v-Txx{kQ)j`})|D;Y_Hgpo+~D_tdePkE=aXxFr*bz5q2A;yGK^q_SO>-~vXH z#LoX;AdvrtS^fu)96^o4Ri@D6t@9xfjQWp5|63^3|K9BT-@~W=|N0H>UvvKh1(rzg literal 139774 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 diff --git a/doc/telnet/telnet_stats.PNG b/doc/telnet/telnet_stats.PNG index 892d66f74673a7a8c26601739e50855741f7f4a1..2e13c456f39c88021172e4f78858d340b4c1a32b 100644 GIT binary patch literal 45112 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 Date: Mon, 18 Mar 2019 22:31:51 +0100 Subject: [PATCH 27/59] uptime shows in HH:MM:SS --- lib/MyESP/MyESP.cpp | 11 ++++++++--- lib/MyESP/MyESP.h | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index 8f94b19dc..bae360526 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -790,7 +790,7 @@ String MyESP::_buildTime() { return String(buffer); } -// returns system uptime - copied for espurna. see (c) +// returns system uptime in seconds - copied for espurna. see (c) unsigned long MyESP::_getUptime() { static unsigned long last_uptime = 0; static unsigned char uptime_overflows = 0; @@ -819,7 +819,12 @@ void MyESP::showSystemStats() { if (_boottime != NULL) { myDebug_P(PSTR(" [APP] Boot time: %s"), _boottime); } - myDebug_P(PSTR(" [APP] Uptime: %d seconds"), _getUptime()); + uint32_t t = _getUptime(); // seconds + uint32_t h = (uint32_t)t / (uint32_t)3600L; + uint32_t rem = (uint32_t)t % (uint32_t)3600L; + uint32_t m = rem / 60; + uint32_t s = rem % 60; + myDebug_P(PSTR(" [APP] Uptime: %d seconds (%02d:%02d:%02d)"), t, h, m, s); myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage()); if (isAPmode()) { @@ -909,7 +914,7 @@ void MyESP::_telnetHandle() { } break; - case '\b': // (^H) handle backspace in input: put a space in last char - coded by Simon Arlott + case '\b': // (^H) case 0x7F: // (^?) if (charsRead > 0) { _command[--charsRead] = '\0'; diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index d2837453f..70dd253ef 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6b1" +#define MYESP_VERSION "1.1.6b2" #include #include From c3204783bce8b0d31c7bd51f5ad0ce333ce7b9b4 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 18 Mar 2019 22:32:09 +0100 Subject: [PATCH 28/59] cleanup --- src/emsuart.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 7b33507ac..6d5a88241 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -67,8 +67,8 @@ static void emsuart_rx_intr_handler(void * para) { /* * system task triggered on BRK interrupt - * Read commands are all asynchronous - * When a buffer is full it is sent to the ems_parseTelegram() function in ems.cpp. This is the hook + * incoming received messages are always asynchronous + * The full buffer is sent to the ems_parseTelegram() function in ems.cpp. */ static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) { // get next free EMS Receive buffer @@ -144,7 +144,7 @@ void ICACHE_FLASH_ATTR emsuart_init() { void ICACHE_FLASH_ATTR emsuart_stop() { ETS_UART_INTR_DISABLE(); //ETS_UART_INTR_ATTACH(NULL, NULL); - //system_uart_swap(); // to be sure, swap Tx/Rx back. Idea from Simon Arlott + //system_uart_swap(); // to be sure, swap Tx/Rx back. //detachInterrupt(digitalPinToInterrupt(D7)); //noInterrupts(); } From 94c923237f4021727b40e95b7c5b3fdbe7840050 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 18 Mar 2019 22:32:37 +0100 Subject: [PATCH 29/59] silent_mode and startup --- src/ems-esp.ino | 61 +++++++++++++--------- src/ems.cpp | 135 ++++++++++++++++++++++++++++++++++-------------- src/ems.h | 17 ++++-- 3 files changed, 148 insertions(+), 65 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index ce967b881..21da5807f 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -67,7 +67,7 @@ typedef struct { bool shower_timer; // true if we want to report back on shower times bool shower_alert; // true if we want the alert of cold water bool led_enabled; // LED on/off - bool test_mode; // test mode to stop automatic Tx on/off + bool silent_mode; // stop automatic Tx on/off uint16_t publish_time; // frequency of MQTT publish in seconds uint8_t led_gpio; uint8_t dallas_gpio; @@ -90,7 +90,7 @@ command_t PROGMEM project_cmds[] = { {true, "dallas_parasite ", "set to on if powering Dallas via parasite"}, {true, "thermostat_type ", "set the thermostat type id (e.g. 10 for 0x10)"}, {true, "boiler_type ", "set the boiler type id (e.g. 8 for 0x08)"}, - {true, "test_mode ", "test_mode turns on/off all automatic reads"}, + {true, "silent_mode ", "when on all automatic Tx is disabled"}, {true, "shower_timer ", "notify via MQTT all shower durations"}, {true, "shower_alert ", "send a warning of cold water after shower time is exceeded"}, {false, "info", "show data captured on the EMS bus"}, @@ -109,7 +109,8 @@ command_t PROGMEM project_cmds[] = { {false, "boiler read ", "send read request to boiler"}, {false, "boiler wwtemp ", "set boiler warm water temperature"}, {false, "boiler tapwater ", "set boiler warm tap water on/off"}, - {false, "boiler comfort ", "set boiler warm water comfort setting"} + {false, "boiler comfort ", "set boiler warm water comfort setting"}, + {false, "startup", "send startup sequence to bus master - still experimental"} }; @@ -275,7 +276,7 @@ void showInfo() { myDebug(" System logging set to None"); } - myDebug(" LED is %s, Test Mode is %s", EMSESP_Status.led_enabled ? "on" : "off", EMSESP_Status.test_mode ? "on" : "off"); + myDebug(" LED is %s, Silent mode is %s", EMSESP_Status.led_enabled ? "on" : "off", EMSESP_Status.silent_mode ? "on" : "off"); myDebug(" # connected Dallas temperature sensors=%d", EMSESP_Status.dallas_sensors); myDebug(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s", @@ -285,8 +286,9 @@ void showInfo() { ((EMSESP_Status.shower_alert) ? "enabled" : "disabled")); myDebug("\n%sEMS Bus stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" Bus Connected=%s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d", + myDebug(" Bus Connected=%s, Tx is %s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d", (ems_getBusConnected() ? "yes" : "no"), + (ems_getTxCapable() ? "active" : "not active"), EMS_Sys_Status.emsRxPgks, EMS_Sys_Status.emsTxPkgs, EMS_Sys_Status.emxCrcErr); @@ -365,7 +367,7 @@ void showInfo() { } _renderFloatValue("Boiler temperature", "C", EMS_Boiler.boilTemp); _renderIntValue("Pump modulation", "%", EMS_Boiler.pumpMod); - _renderLongValue("Burner # restarts", "times", EMS_Boiler.burnStarts); + _renderLongValue("Burner # starts", "times", EMS_Boiler.burnStarts); if (EMS_Boiler.burnWorkMin != EMS_VALUE_LONG_NOTSET) { myDebug(" Total burner operating time: %d days %d hours %d minutes", EMS_Boiler.burnWorkMin / 1440, @@ -685,9 +687,12 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { } // test mode - if (!(EMSESP_Status.test_mode = json["test_mode"])) { - EMSESP_Status.test_mode = false; // default value - recreate_config = true; + if (!(EMSESP_Status.silent_mode = json["silent_mode"])) { + EMSESP_Status.silent_mode = false; // default value + ems_setTxDisabled(false); + recreate_config = true; + } else { + ems_setTxDisabled(true); // silent_mpde is on } // shower_timer @@ -718,7 +723,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { json["dallas_parasite"] = EMSESP_Status.dallas_parasite; json["thermostat_type"] = EMS_Thermostat.type_id; json["boiler_type"] = EMS_Boiler.type_id; - json["test_mode"] = EMSESP_Status.test_mode; + json["silent_mode"] = EMSESP_Status.silent_mode; json["shower_timer"] = EMSESP_Status.shower_timer; json["shower_alert"] = EMSESP_Status.shower_alert; json["publish_time"] = EMSESP_Status.publish_time; @@ -753,16 +758,19 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } // test mode - if ((strcmp(setting, "test_mode") == 0) && (wc == 2)) { + if ((strcmp(setting, "silent_mode") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { - EMSESP_Status.test_mode = true; - ok = true; - myDebug("* Reboot to go into test mode."); + EMSESP_Status.silent_mode = true; + ok = true; + myDebug("* in Silent mode. All Tx is disabled."); + ems_setTxDisabled(true); } else if (strcmp(value, "off") == 0) { - EMSESP_Status.test_mode = false; - ok = true; + EMSESP_Status.silent_mode = false; + ok = true; + ems_setTxDisabled(false); + myDebug("* out of Silent mode. Tx is enabled."); } else { - myDebug("Error. Usage: set test_mode "); + myDebug("Error. Usage: set silent_mode "); } } @@ -859,7 +867,7 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug(" boiler_type=%02X", EMS_Boiler.type_id); } - myDebug(" test_mode=%s", EMSESP_Status.test_mode ? "on" : "off"); + myDebug(" silent_mode=%s", EMSESP_Status.silent_mode ? "on" : "off"); myDebug(" shower_timer=%s", EMSESP_Status.shower_timer ? "on" : "off"); myDebug(" shower_alert=%s", EMSESP_Status.shower_alert ? "on" : "off"); myDebug(" publish_time=%d", EMSESP_Status.publish_time); @@ -910,6 +918,11 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { ok = true; } + if (strcmp(first_cmd, "startup") == 0) { + ems_startupTelegrams(); + ok = true; + } + // shower settings if ((strcmp(first_cmd, "shower") == 0) && (wc == 2)) { char * second_cmd = _readWord(); @@ -1140,8 +1153,10 @@ void WIFICallback() { } else { emsuart_init(); myDebug("[UART] Opened Rx/Tx connection"); - // go and find the boiler and thermostat types - ems_discoverModels(); + if (!EMSESP_Status.silent_mode) { + // go and find the boiler and thermostat types, if not in silent mode + ems_discoverModels(); + } } } @@ -1152,7 +1167,7 @@ void initEMSESP() { EMSESP_Status.shower_timer = false; EMSESP_Status.shower_alert = false; EMSESP_Status.led_enabled = true; // LED is on by default - EMSESP_Status.test_mode = false; + EMSESP_Status.silent_mode = false; EMSESP_Status.publish_time = DEFAULT_PUBLISHVALUES_TIME; EMSESP_Status.timestamp = millis(); @@ -1354,7 +1369,7 @@ void setup() { // at this point we have the settings from our internall SPIFFS config file // enable regular checks if not in test mode - if (!EMSESP_Status.test_mode) { + if (!EMSESP_Status.silent_mode) { publishValuesTimer.attach(EMSESP_Status.publish_time, do_publishValues); // post MQTT EMS values publishSensorValuesTimer.attach(EMSESP_Status.publish_time, do_publishSensorValues); // post MQTT sensor values regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS @@ -1387,7 +1402,7 @@ void loop() { // publish the values to MQTT, only if the values have changed // although we don't want to publish when doing a deep scan of the thermostat - if (ems_getEmsRefreshed() && (scanThermostat_count == 0) && (!EMSESP_Status.test_mode)) { + if (ems_getEmsRefreshed() && (scanThermostat_count == 0) && (!EMSESP_Status.silent_mode)) { publishValues(false); ems_setEmsRefreshed(false); // reset } diff --git a/src/ems.cpp b/src/ems.cpp index 1724e2b76..1db816e0b 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -118,7 +118,7 @@ const _EMS_Type EMS_Types[] = { }; -// calculate sizes of arrays +// calculate sizes of arrays at compile uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of models uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types @@ -162,6 +162,7 @@ void ems_init() { EMS_Sys_Status.emsBusConnected = false; EMS_Sys_Status.emsRxTimestamp = 0; EMS_Sys_Status.emsTxCapable = false; + EMS_Sys_Status.emsTxDisabled = false; EMS_Sys_Status.emsPollTimestamp = 0; EMS_Sys_Status.txRetryCount = 0; @@ -234,12 +235,12 @@ void ems_init() { // set boiler type EMS_Boiler.product_id = 0; - strlcpy(EMS_Boiler.version, "not set", sizeof(EMS_Boiler.version)); + strlcpy(EMS_Boiler.version, "?", sizeof(EMS_Boiler.version)); // set thermostat model EMS_Thermostat.model_id = EMS_MODEL_NONE; EMS_Thermostat.product_id = 0; - strlcpy(EMS_Thermostat.version, "not set", sizeof(EMS_Thermostat.version)); + strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version)); // default logging is none ems_setLogging(EMS_SYS_LOGGING_DEFAULT); @@ -275,6 +276,10 @@ uint8_t ems_getThermostatModel() { return (EMS_Thermostat.model_id); } +void ems_setTxDisabled(bool b) { + EMS_Sys_Status.emsTxDisabled = b; +} + bool ems_getTxCapable() { if ((millis() - EMS_Sys_Status.emsPollTimestamp) > EMS_POLL_TIMEOUT) { EMS_Sys_Status.emsTxCapable = false; @@ -392,7 +397,7 @@ char * _smallitoa(uint8_t value, char * buffer) { } /* for decimals 0 to 999, printed as a string - * From Simon Arlott @nomis + * From @nomis */ char * _smallitoa3(uint16_t value, char * buffer) { buffer[0] = ((value / 100) == 0) ? '0' : (value / 100) + '0'; @@ -406,24 +411,24 @@ char * _smallitoa3(uint16_t value, char * buffer) { * debug print a telegram to telnet/serial including the CRC * len is length in bytes including the CRC */ -void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const char * color) { +void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram EMS_RxTelegram, const char * color) { if (EMS_Sys_Status.emsLogging <= EMS_SYS_LOGGING_BASIC) return; - char output_str[200] = {0}; - char buffer[16] = {0}; + char output_str[200] = {0}; + char buffer[16] = {0}; + uint8_t len = EMS_RxTelegram.length; + uint8_t * data = EMS_RxTelegram.telegram; - unsigned long upt = millis(); - strlcpy(output_str, "(", sizeof(output_str)); strlcat(output_str, COLOR_CYAN, sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((upt / 3600000) % 24), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram.timestamp / 3600000) % 24), buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((upt / 60000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram.timestamp / 60000) % 60), buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((upt / 1000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram.timestamp / 1000) % 60), buffer), sizeof(output_str)); strlcat(output_str, ".", sizeof(output_str)); - strlcat(output_str, _smallitoa3(upt % 1000, buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa3(EMS_RxTelegram.timestamp % 1000, buffer), sizeof(output_str)); strlcat(output_str, COLOR_RESET, sizeof(output_str)); strlcat(output_str, ") ", sizeof(output_str)); @@ -468,9 +473,13 @@ void _ems_sendTelegram() { // if we're in raw mode just fire and forget if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_RAW) { EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC - _debugPrintTelegram("Sending raw", EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); // always show - emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx - EMS_TxQueue.shift(); // remove from queue + _EMS_RxTelegram EMS_RxTelegram; + EMS_RxTelegram.length = EMS_TxTelegram.length; + EMS_RxTelegram.telegram = EMS_TxTelegram.data; + EMS_RxTelegram.timestamp = millis(); // now + _debugPrintTelegram("Sending raw", EMS_RxTelegram, COLOR_CYAN); // always show + emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx + EMS_TxQueue.shift(); // remove from queue return; } @@ -514,7 +523,11 @@ void _ems_sendTelegram() { snprintf(s, sizeof(s), "Sending validate of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } - _debugPrintTelegram(s, EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); + _EMS_RxTelegram EMS_RxTelegram; + EMS_RxTelegram.length = EMS_TxTelegram.length; + EMS_RxTelegram.telegram = EMS_TxTelegram.data; + EMS_RxTelegram.timestamp = millis(); // now + _debugPrintTelegram(s, EMS_RxTelegram, COLOR_CYAN); } // send the telegram to the UART Tx @@ -577,12 +590,19 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // check if we just received a single byte // it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) // or either a return code like 0x01 or 0x04 from the last Write command + + // create the Rx package + _EMS_RxTelegram EMS_RxTelegram; + EMS_RxTelegram.length = length; + EMS_RxTelegram.telegram = telegram; + EMS_RxTelegram.timestamp = millis(); + if (length == 1) { uint8_t value = telegram[0]; // 1st byte of data package // check first for a Poll for us if (value == (EMS_ID_ME | 0x80)) { - EMS_Sys_Status.emsPollTimestamp = millis(); // store when we received a last poll + EMS_Sys_Status.emsPollTimestamp = EMS_RxTelegram.timestamp; // store when we received a last poll EMS_Sys_Status.emsTxCapable = true; // do we have something to send thats waiting in the Tx queue? @@ -628,7 +648,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { if (telegram[length - 1] != crc) { EMS_Sys_Status.emxCrcErr++; if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - _debugPrintTelegram("Corrupt telegram:", telegram, length, COLOR_RED); + _debugPrintTelegram("Corrupt telegram:", EMS_RxTelegram, COLOR_RED); } return; } @@ -647,24 +667,25 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // here we know its a valid incoming telegram of at least 6 bytes // we use this to see if we always have a connection to the boiler, in case of drop outs - EMS_Sys_Status.emsRxTimestamp = millis(); // timestamp of last read + EMS_Sys_Status.emsRxTimestamp = EMS_RxTelegram.timestamp; // timestamp of last read EMS_Sys_Status.emsBusConnected = true; // now lets process it and see what to do next - _processType(telegram, length); + _processType(EMS_RxTelegram); } /** * print detailed telegram * and then call its callback if there is one defined */ -void _ems_processTelegram(uint8_t * telegram, uint8_t length) { +void _ems_processTelegram(_EMS_RxTelegram EMS_RxTelegram) { // header - uint8_t src = telegram[0] & 0x7F; - uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes - uint8_t type = telegram[2]; - uint8_t offset = telegram[3]; - uint8_t * data = telegram + 4; // data block starts at position 5 + uint8_t * telegram = EMS_RxTelegram.telegram; + uint8_t src = telegram[0] & 0x7F; + uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes + uint8_t type = telegram[2]; + uint8_t offset = telegram[3]; + uint8_t * data = telegram + 4; // data block starts at position 5 // print detailed telegram data if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { @@ -710,11 +731,11 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_THERMOSTAT) { // only print ones to/from thermostat if logging is set to thermostat only if ((src == EMS_Thermostat.type_id) || (dest == EMS_Thermostat.type_id)) { - _debugPrintTelegram(output_str, telegram, length, color_s); + _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); } } else { // always print - _debugPrintTelegram(output_str, telegram, length, color_s); + _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); } } @@ -746,7 +767,7 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { // call callback function to process it // as we only handle complete telegrams (not partial) check that the offset is 0 if (offset == EMS_ID_NONE) { - (void)EMS_Types[i].processType_cb(type, data, length - 5); + (void)EMS_Types[i].processType_cb(type, data, EMS_RxTelegram.length - 5); } } } @@ -767,19 +788,22 @@ void _removeTxQueue() { * length is only data bytes, excluding the BRK * We only remove from the Tx queue if the read or write was successful */ -void _processType(uint8_t * telegram, uint8_t length) { +void _processType(_EMS_RxTelegram EMS_RxTelegram) { + uint8_t * telegram = EMS_RxTelegram.telegram; + uint8_t length = EMS_RxTelegram.length; + // header uint8_t src = telegram[0] & 0x7F; // removing 8th bit as we deal with both reads and writes here // if its an echo of ourselves from the master UBA, ignore if (src == EMS_ID_ME) { - //_debugPrintTelegram("Telegram echo:", telegram, length, COLOR_BLUE); + _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE); return; } // if its a broadcast and we didn't just send anything, process it and exit if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE) { - _ems_processTelegram(telegram, length); + _ems_processTelegram(EMS_RxTelegram); return; } @@ -791,13 +815,13 @@ void _processType(uint8_t * telegram, uint8_t length) { // and if not we probably didn't get any response so remove the last Tx from the queue and process the telegram anyway if ((telegram[1] & 0x7F) != EMS_ID_ME) { _removeTxQueue(); - _ems_processTelegram(telegram, length); + _ems_processTelegram(EMS_RxTelegram); return; } // first double check we actually have something in the queue if (EMS_TxQueue.isEmpty()) { - _ems_processTelegram(telegram, length); + _ems_processTelegram(EMS_RxTelegram); return; } @@ -832,7 +856,7 @@ void _processType(uint8_t * telegram, uint8_t length) { } } } - _ems_processTelegram(telegram, length); // process it always + _ems_processTelegram(EMS_RxTelegram); // process it always } if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { @@ -1128,7 +1152,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { if (typeFound) { // its a boiler - myDebug("Boiler type device found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + myDebug("Boiler found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", Boiler_Types[i].model_string, Boiler_Types[i].type_id, product_id, @@ -1198,7 +1222,6 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { } else { myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", type, product_id, version); } - } /* @@ -1531,6 +1554,11 @@ void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh) { return; } + // if we're preventing all outbound traffic, quit + if (EMS_Sys_Status.emsTxDisabled) { + return; + } + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter @@ -1569,6 +1597,10 @@ void ems_sendRawTelegram(char * telegram) { char * p; char value[10] = {0}; + if (EMS_Sys_Status.emsTxDisabled) { + return; // user has disabled all Tx + } + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter @@ -1594,6 +1626,10 @@ void ems_sendRawTelegram(char * telegram) { } } + if (count == 0) { + return; // nothing to send + } + // calculate length including header and CRC EMS_TxTelegram.length = count + 2; EMS_TxTelegram.type_validate = EMS_ID_NONE; @@ -1845,3 +1881,26 @@ void ems_setWarmTapWaterActivated(bool activated) { EMS_TxQueue.push(EMS_TxTelegram); // add to queue } + +/* + * Start up sequence for UBA Master + * Still experimental + */ +void ems_startupTelegrams() { + if ((EMS_Sys_Status.emsTxDisabled) || (!EMS_Sys_Status.emsBusConnected)) { + myDebug("Unable to send startup sequence when in silent mode or bus is disabled"); + } + + myDebug("Sending startup sequence..."); + char s[20] = {0}; + + // (00:07:27.512) Telegram echo: telegram: 0B 08 1D 00 00 (CRC=84), #data=1 + // Write type 0x1D to get out of function test mode + snprintf(s, sizeof(s), "%02X %02X 1D 00 00", EMS_ID_ME, EMS_Boiler.type_id); + ems_sendRawTelegram(s); + + // (00:07:35.555) Telegram echo: telegram: 0B 88 01 00 1B (CRC=8B), #data=1 + // Read type 0x01 + snprintf(s, sizeof(s), "%02X %02X 01 00 1B", EMS_ID_ME, EMS_Boiler.type_id | 0x80); + ems_sendRawTelegram(s); +} diff --git a/src/ems.h b/src/ems.h index 0b4dc20be..8fb3caa94 100644 --- a/src/ems.h +++ b/src/ems.h @@ -92,6 +92,7 @@ typedef struct { unsigned long emsRxTimestamp; // timestamp of last EMS message received unsigned long emsPollTimestamp; // timestamp of last EMS poll sent to us bool emsTxCapable; // able to send via Tx + bool emsTxDisabled; // true to prevent all Tx uint8_t txRetryCount; // # times the last Tx was re-sent } _EMS_Sys_Status; @@ -112,6 +113,12 @@ typedef struct { uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; } _EMS_TxTelegram; +// The Rx receive package +typedef struct { + uint32_t timestamp; // timestamp from millis() + uint8_t * telegram; // the full data package + uint8_t length; // length in bytes +} _EMS_RxTelegram; // default empty Tx @@ -179,7 +186,7 @@ typedef struct { // UBAParameterWW float extTemp; // Outside temperature float boilTemp; // Boiler temperature uint8_t pumpMod; // Pump modulation - uint32_t burnStarts; // # burner restarts + uint32_t burnStarts; // # burner starts uint32_t burnWorkMin; // Total burner operating time uint32_t heatWorkMin; // Total heat operating time @@ -251,12 +258,12 @@ void ems_setWarmWaterTemp(uint8_t temperature); void ems_setWarmWaterActivated(bool activated); void ems_setWarmTapWaterActivated(bool activated); void ems_setPoll(bool b); -void ems_setTxEnabled(bool b); void ems_setLogging(_EMS_SYS_LOGGING loglevel); void ems_setEmsRefreshed(bool b); void ems_setWarmWaterModeComfort(uint8_t comfort); bool ems_checkEMSBUSAlive(); void ems_setModels(); +void ems_setTxDisabled(bool b); void ems_getThermostatValues(); void ems_getBoilerValues(); @@ -277,10 +284,12 @@ char * ems_getThermostatDescription(char * buffer); void ems_printTxQueue(); char * ems_getBoilerDescription(char * buffer); +void ems_startupTelegrams(); + // private functions uint8_t _crcCalculator(uint8_t * data, uint8_t len); -void _processType(uint8_t * telegram, uint8_t length); -void _debugPrintPackage(const char * prefix, uint8_t * data, uint8_t len, const char * color); +void _processType(_EMS_RxTelegram EMS_RxTelegram); +void _debugPrintPackage(const char * prefix, _EMS_RxTelegram EMS_RxTelegram, const char * color); void _ems_clearTxData(); int _ems_findBoilerModel(uint8_t model_id); bool _ems_setModel(uint8_t model_id); From ec46a434fb0fe8d6a9e53051f491305529258a74 Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 18 Mar 2019 23:00:04 +0100 Subject: [PATCH 30/59] fixed bug with set led --- lib/MyESP/MyESP.cpp | 7 +++--- src/ems-esp.ino | 53 +++++++++++++++------------------------------ 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index bae360526..9a1ff6d76 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -505,7 +505,7 @@ void MyESP::_printSetCommands() { myDebug_P(PSTR("* set erase")); myDebug_P(PSTR("* set wifi [ssid] [password]")); myDebug_P(PSTR("* set [value]")); - myDebug_P(PSTR("* set serial")); + myDebug_P(PSTR("* set serial ")); // print custom commands if available. Taken from progmem if (_telnetcommand_callback) { @@ -679,7 +679,7 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) myDebug_P(PSTR("")); // newline - (void)fs_saveConfig(); + (void)fs_saveConfig(); // always save the values } void MyESP::_telnetCommand(char * commandLine) { @@ -1244,7 +1244,6 @@ bool MyESP::fs_saveConfig() { // init the SPIFF file system and load the config // if it doesn't exist try and create it -// force Serial for debugging, and turn it off afterwards void MyESP::_fs_setup() { if (!SPIFFS.begin()) { myDebug_P(PSTR("[FS] Failed to mount the file system. Erasing...")); @@ -1258,7 +1257,7 @@ void MyESP::_fs_setup() { fs_saveConfig(); } - //_fs_printConfig(); // enable for debugging + // _fs_printConfig(); // enable for debugging } uint16_t MyESP::getSystemLoadAverage() { diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 21da5807f..330fe775e 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -66,7 +66,7 @@ typedef struct { // custom params bool shower_timer; // true if we want to report back on shower times bool shower_alert; // true if we want the alert of cold water - bool led_enabled; // LED on/off + bool led; // LED on/off bool silent_mode; // stop automatic Tx on/off uint16_t publish_time; // frequency of MQTT publish in seconds uint8_t led_gpio; @@ -276,7 +276,7 @@ void showInfo() { myDebug(" System logging set to None"); } - myDebug(" LED is %s, Silent mode is %s", EMSESP_Status.led_enabled ? "on" : "off", EMSESP_Status.silent_mode ? "on" : "off"); + myDebug(" LED is %s, Silent mode is %s", EMSESP_Status.led ? "on" : "off", EMSESP_Status.silent_mode ? "on" : "off"); myDebug(" # connected Dallas temperature sensors=%d", EMSESP_Status.dallas_sensors); myDebug(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s", @@ -647,77 +647,61 @@ void startThermostatScan(uint8_t start) { // callback for loading/saving settings to the file system (SPIFFS) bool FSCallback(MYESP_FSACTION action, const JsonObject json) { - bool recreate_config = false; + bool recreate_config = true; if (action == MYESP_FSACTION_LOAD) { // led - if (!(EMSESP_Status.led_enabled = json["led"])) { - EMSESP_Status.led_enabled = LED_BUILTIN; // default value - recreate_config = true; - } + EMSESP_Status.led = json["led"]; // led_gpio if (!(EMSESP_Status.led_gpio = json["led_gpio"])) { EMSESP_Status.led_gpio = EMSESP_LED_GPIO; // default value - recreate_config = true; } // dallas_gpio if (!(EMSESP_Status.dallas_gpio = json["dallas_gpio"])) { EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; // default value - recreate_config = true; } // dallas_parasite if (!(EMSESP_Status.dallas_parasite = json["dallas_parasite"])) { EMSESP_Status.dallas_parasite = EMSESP_DALLAS_PARASITE; // default value - recreate_config = true; } // thermostat_type if (!(EMS_Thermostat.type_id = json["thermostat_type"])) { EMS_Thermostat.type_id = EMSESP_THERMOSTAT_TYPE; // set default - recreate_config = true; } // boiler_type if (!(EMS_Boiler.type_id = json["boiler_type"])) { EMS_Boiler.type_id = EMSESP_BOILER_TYPE; // set default - recreate_config = true; } - // test mode - if (!(EMSESP_Status.silent_mode = json["silent_mode"])) { - EMSESP_Status.silent_mode = false; // default value - ems_setTxDisabled(false); - recreate_config = true; - } else { - ems_setTxDisabled(true); // silent_mpde is on - } + // silent mode + EMSESP_Status.silent_mode = json["silent_mode"]; + ems_setTxDisabled(EMSESP_Status.silent_mode); // shower_timer if (!(EMSESP_Status.shower_timer = json["shower_timer"])) { EMSESP_Status.shower_timer = false; // default value - recreate_config = true; } // shower_alert if (!(EMSESP_Status.shower_alert = json["shower_alert"])) { EMSESP_Status.shower_alert = false; // default value - recreate_config = true; } // publish_time if (!(EMSESP_Status.publish_time = json["publish_time"])) { EMSESP_Status.publish_time = DEFAULT_PUBLISHVALUES_TIME; // default value - recreate_config = true; } return recreate_config; // return false if some settings are missing and we need to rebuild the file } if (action == MYESP_FSACTION_SAVE) { - json["led"] = EMSESP_Status.led_enabled; + json["led"] = EMSESP_Status.led; json["led_gpio"] = EMSESP_Status.led_gpio; json["dallas_gpio"] = EMSESP_Status.dallas_gpio; json["dallas_parasite"] = EMSESP_Status.dallas_parasite; @@ -736,7 +720,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { // callback for custom settings when showing Stored Settings with the 'set' command // wc is number of arguments after the 'set' command -// returns true if the setting was recognized and changed +// returns true if the setting was recognized and changed and should be saved back to SPIFFs bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) { bool ok = false; @@ -744,14 +728,13 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c // led if ((strcmp(setting, "led") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { - EMSESP_Status.led_enabled = true; - ok = true; + EMSESP_Status.led = true; + ok = true; } else if (strcmp(value, "off") == 0) { - EMSESP_Status.led_enabled = false; - ok = true; - // let's make sure LED is really off - digitalWrite(EMSESP_Status.led_gpio, - (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off + EMSESP_Status.led = false; + ok = true; + // let's make sure LED is really off - For onboard high=off + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); } else { myDebug("Error. Usage: set led "); } @@ -848,7 +831,7 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } if (action == MYESP_FSACTION_LIST) { - myDebug(" led=%s", EMSESP_Status.led_enabled ? "on" : "off"); + myDebug(" led=%s", EMSESP_Status.led ? "on" : "off"); myDebug(" led_gpio=%d", EMSESP_Status.led_gpio); myDebug(" dallas_gpio=%d", EMSESP_Status.dallas_gpio); myDebug(" dallas_parasite=%s", EMSESP_Status.dallas_parasite ? "on" : "off"); @@ -1166,7 +1149,7 @@ void initEMSESP() { // general settings EMSESP_Status.shower_timer = false; EMSESP_Status.shower_alert = false; - EMSESP_Status.led_enabled = true; // LED is on by default + EMSESP_Status.led = true; // LED is on by default EMSESP_Status.silent_mode = false; EMSESP_Status.publish_time = DEFAULT_PUBLISHVALUES_TIME; @@ -1201,7 +1184,7 @@ void do_publishValues() { // callback to light up the LED, called via Ticker every second // fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off void do_ledcheck() { - if (EMSESP_Status.led_enabled) { + if (EMSESP_Status.led) { if (ems_getBusConnected()) { digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off } else { From 1cfdd23434ebe2d0546e81c95eb890832aea8d36 Mon Sep 17 00:00:00 2001 From: Proddy Date: Tue, 19 Mar 2019 11:21:48 +0100 Subject: [PATCH 31/59] typo --- platformio.ini-example | 2 -- 1 file changed, 2 deletions(-) diff --git a/platformio.ini-example b/platformio.ini-example index 1fde98e32..7e74d40e4 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -2,8 +2,6 @@ ; add here your board, e.g. nodemcuv2, d1_mini, d1_mini_pro env_default = d1_mini -[common] -platform = espressif8266 [common] platform_def = espressif8266 platform_180 = espressif8266@1.8.0 From 34f16c5a3ff39a8c3cf8cae6bf5af4c1caabd70d Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 19 Mar 2019 20:01:03 +0100 Subject: [PATCH 32/59] set wifi replaced --- CHANGELOG.md | 3 +- README.md | 6 +- lib/MyESP/MyESP.cpp | 142 ++++++++++++++++++++++---------------------- lib/MyESP/MyESP.h | 7 +-- 4 files changed, 79 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83db7685f..b5fbf823e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.5.7 dev] 2019-03- +## [1.6.0 dev] 2019-03-19 ### Added @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - included various fixes and suggestions from @nomis - upgraded MyESP library - test_mode renamed to silent_mode +- 'set wifi' replaced with 'set wifi_ssid and set wifi_password' to allow values with spaces ## [1.5.6] 2019-03-09 diff --git a/README.md b/README.md index bc00771bf..586fa3e8b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ EMS-ESP is a project to build an electronic controller circuit using an Espressi There are 3 parts to this project, first the design of the circuit, secondly the code for the ESP8266 microcontroller firmware with telnet and MQTT support, and lastly an example configuration for Home Assistant to monitor the data and issue direct commands via a MQTT broker. [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b8880625bdf841d4adb2829732030887)](https://app.codacy.com/app/proddy/EMS-ESP?utm_source=github.com&utm_medium=referral&utm_content=proddy/EMS-ESP&utm_campaign=Badge_Grade_Settings) -[![version](https://img.shields.io/badge/version-1.5.5-brightgreen.svg)](CHANGELOG.md) +[![version](https://img.shields.io/badge/version-1.6.0-brightgreen.svg)](CHANGELOG.md) - [EMS-ESP](#ems-esp) - [Introduction](#introduction) @@ -69,7 +69,7 @@ The code and circuit has been tested with a few ESP8266 development boards such 5. Connect an external USB 5v power adapter to the ESP8266 board. 7. When the ESP8266 starts up for the first time the onboard LED will be flashing. This is because the EMS bus is not yet connected and receiving data. 8. If you haven't hardcoded the WiFi credentials in step 4, the ESP8266 will boot up in a WiFi Access Point (AP) mode with the ssid name `ems-esp`. Now you can either use a laptop and connect to this AP using Telnet to `192.168.1.4` or if its powered from a computers USB use a Serial monitor tool to the ESP's COM port. Tip: to enable Telnet on Windows 10 run `dism /online /Enable-Feature /FeatureName:TelnetClient` or install something like [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). -9. Next is to customize some of the onboard settings. Type `set` to list the current stored settings and `?` to see the syntax. Use `set wifi` to add your wifi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the ESP. +9. Next is to customize some of the onboard settings. Type `set` to list the current stored settings and `?` to see the syntax. Use `set wifi_ssid` and `set wifi_password` to add your WiFi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the ESP. 10. The `led_gpio` will default to the onboard LED (which is probably blinking now). Ignore `thermostat_type` and `boiler_type` as these will be auto-detected hopefully later on. 11. **Important**: By default the serial port is enabled and the EMS bus disabled. This is to allow users to configure their ESP via the serial monitor when pluged into a PC/laptop. You must disable serial with `set serial off` to get the EMS transmission working. 12. Hook up the ESP to the EMS board as follows: @@ -330,7 +330,7 @@ pre-baked firmware for the Wemos D1 mini is available in the GitHub [releases](h 1. Check if you have **python 2.7** installed. If not [download it](https://www.python.org/downloads/) and make sure you select the option to add Python to the windows PATH 2. Then install the ESPTool by running `pip install esptool` from a command prompt -The ESP8266 will start in Access Point (AP) mode. Connect via WiFi to the SSID **EMS-ESP** and telnet to **192.168.4.1**. Then use the `set wifi` command to configure your own network settings like `set wifi your_ssid your_password`. Alternatively connect the ESP8266 to your PC and open a Serial monitor (with baud 115200) to configure the settings. Make sure you disable Serial support before connecting the EMS lines using `set serial off`. +The ESP8266 will start in Access Point (AP) mode. Connect via WiFi to the SSID **EMS-ESP** and telnet to **192.168.4.1**. Then use the `set wifi_ssid/set wifi_password` command to configure your own network settings. Alternatively connect the ESP8266 to your PC and open a Serial monitor (with baud 115200) to configure the settings. Make sure you disable Serial support before connecting the EMS lines using `set serial off`. `set` wil list all currently stored settings. diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index 9a1ff6d76..a42996251 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -503,7 +503,7 @@ void MyESP::_printSetCommands() { myDebug_P(PSTR("The following set commands are available:")); myDebug_P(PSTR("")); // newline myDebug_P(PSTR("* set erase")); - myDebug_P(PSTR("* set wifi [ssid] [password]")); + myDebug_P(PSTR("* set [value]")); myDebug_P(PSTR("* set [value]")); myDebug_P(PSTR("* set serial ")); @@ -532,12 +532,14 @@ void MyESP::_printSetCommands() { myDebug_P(PSTR("")); // newline myDebug_P(PSTR("Stored settings:")); myDebug_P(PSTR("")); // newline - SerialAndTelnet.printf(PSTR(" wifi=%s "), (!_wifi_ssid) ? "" : _wifi_ssid); + myDebug_P(PSTR(" wifi_ssid=%s "), (!_wifi_ssid) ? "" : _wifi_ssid); + SerialAndTelnet.print(FPSTR(" wifi_password=")); if (!_wifi_password) { SerialAndTelnet.print(FPSTR("")); } else { - for (uint8_t i = 0; i < strlen(_wifi_password); i++) + for (uint8_t i = 0; i < strlen(_wifi_password); i++) { SerialAndTelnet.print(FPSTR("*")); + } } myDebug_P(PSTR("")); // newline myDebug_P(PSTR(" mqtt_host=%s"), (!_mqtt_host) ? "" : _mqtt_host); @@ -546,8 +548,9 @@ void MyESP::_printSetCommands() { if (!_mqtt_password) { SerialAndTelnet.print(FPSTR("")); } else { - for (uint8_t i = 0; i < strlen(_mqtt_password); i++) + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) { SerialAndTelnet.print(FPSTR("*")); + } } myDebug_P(PSTR("")); // newline @@ -571,54 +574,47 @@ void MyESP::resetESP() { } // read next word from string buffer -char * MyESP::_telnet_readWord() { - return (strtok(NULL, ", \n")); -} - -// change setting for 2 params (set ) -void MyESP::_changeSetting2(const char * setting, const char * value1, const char * value2) { - if (strcmp(setting, "wifi") == 0) { - if (_wifi_ssid) - free(_wifi_ssid); - if (_wifi_password) - free(_wifi_password); - _wifi_ssid = NULL; - _wifi_password = NULL; - - if (value1) { - _wifi_ssid = strdup(value1); - } - - if (value2) { - _wifi_password = strdup(value2); - } - - (void)fs_saveConfig(); - myDebug_P(PSTR("WiFi settings changed. Reboot ESP.")); - //jw.disconnect(); - //jw.cleanNetworks(); - //jw.addNetwork(_wifi_ssid, _wifi_password); +// if parameter true then a word is only terminated by a newline +char * MyESP::_telnet_readWord(bool allow_all_chars) { + if (allow_all_chars) { + return (strtok(NULL, "\n")); // allow only newline + } else { + return (strtok(NULL, ", \n")); // allow space and comma } } // change settings - always as strings // messy code but effective since we don't have too many settings // wc is word count, number of parameters after the 'set' command -void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { +bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { bool ok = false; // check for our internal commands first if (strcmp(setting, "erase") == 0) { _fs_eraseConfig(); - return; - } else if ((strcmp(setting, "wifi") == 0) && (wc == 1)) { // erase wifi settings + return true; + + } else if (strcmp(setting, "wifi_ssid") == 0) { if (_wifi_ssid) free(_wifi_ssid); + _wifi_ssid = NULL; // just to be sure + if (value) { + _wifi_ssid = strdup(value); + } + ok = true; + jw.enableSTA(false); + myDebug_P(PSTR("Note: please reboot to apply new WiFi settings")); + } else if (strcmp(setting, "wifi_password") == 0) { if (_wifi_password) free(_wifi_password); - _wifi_ssid = NULL; - _wifi_password = NULL; - ok = true; + _wifi_password = NULL; // just to be sure + if (value) { + _wifi_password = strdup(value); + } + ok = true; + jw.enableSTA(false); + myDebug_P(PSTR("Note: please reboot to apply new WiFi settings")); + } else if (strcmp(setting, "mqtt_host") == 0) { if (_mqtt_host) free(_mqtt_host); @@ -643,6 +639,7 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) _mqtt_password = strdup(value); } ok = true; + } else if (strcmp(setting, "serial") == 0) { ok = true; _use_serial = false; @@ -664,29 +661,30 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); } - if (!ok) { - myDebug_P(PSTR("\nInvalid parameter for set command.")); - return; + // if we were able to recognize the set command, continue + if (ok) { + // check for 2 params + if (value == nullptr) { + myDebug_P(PSTR("%s setting reset to its default value."), setting); + } else { + // must be 3 params + myDebug_P(PSTR("%s changed."), setting); + } + + myDebug_P(PSTR("")); // newline + + (void)fs_saveConfig(); // always save the values } - // check for 2 params - if (value == nullptr) { - myDebug_P(PSTR("%s setting reset to its default value."), setting); - } else { - // must be 3 params - myDebug_P(PSTR("%s changed."), setting); - } - - myDebug_P(PSTR("")); // newline - - (void)fs_saveConfig(); // always save the values + return ok; } void MyESP::_telnetCommand(char * commandLine) { + char * str = commandLine; + bool state = false; + // count the number of arguments - char * str = commandLine; - bool state = false; - unsigned wc = 0; + unsigned wc = 0; while (*str) { if (*str == ' ' || *str == '\n' || *str == '\t') { state = false; @@ -698,26 +696,28 @@ void MyESP::_telnetCommand(char * commandLine) { } // check first for reserved commands - char * temp = strdup(commandLine); // because strotok kills original string buffer - char * ptrToCommandName = strtok((char *)temp, ", \n"); + char * temp = strdup(commandLine); // because strotok kills original string buffer + char * ptrToCommandName = strtok((char *)temp, " \n"); // space and newline // set command if (strcmp(ptrToCommandName, "set") == 0) { + bool ok = false; if (wc == 1) { _printSetCommands(); - } else if (wc == 2) { - char * setting = _telnet_readWord(); - _changeSetting(1, setting, NULL); - } else if (wc == 3) { - char * setting = _telnet_readWord(); - char * value = _telnet_readWord(); - _changeSetting(2, setting, value); - } else if (wc == 4) { - char * setting = _telnet_readWord(); - char * value1 = _telnet_readWord(); - char * value2 = _telnet_readWord(); - _changeSetting2(setting, value1, value2); + ok = true; + } else if (wc == 2) { // set + char * setting = _telnet_readWord(false); + ok = _changeSetting(wc - 1, setting, NULL); + } else { // set + char * setting = _telnet_readWord(false); + char * value = _telnet_readWord(true); // allow strange characters + ok = _changeSetting(wc - 1, setting, value); } + + if (!ok) { + myDebug_P(PSTR("\nInvalid parameter for set command.")); + } + return; } @@ -735,13 +735,13 @@ void MyESP::_telnetCommand(char * commandLine) { // crash command #ifdef CRASH if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { - char * cmd = _telnet_readWord(); + char * cmd = _telnet_readWord(false); if (strcmp(cmd, "dump") == 0) { crashDump(); } else if (strcmp(cmd, "clear") == 0) { crashClear(); } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) { - char * value = _telnet_readWord(); + char * value = _telnet_readWord(false); crashTest(atoi(value)); } return; // don't call custom command line callback @@ -1253,7 +1253,7 @@ void MyESP::_fs_setup() { // load the config file. if it doesn't exist (function returns false) create it if (!_fs_loadConfig()) { - myDebug_P(PSTR("[FS] Re-creating config file")); + //myDebug_P(PSTR("[FS] Re-creating config file")); fs_saveConfig(); } diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 70dd253ef..23f1cc455 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6b2" +#define MYESP_VERSION "1.1.6b3" #include #include @@ -242,7 +242,7 @@ class MyESP { void _telnetDisconnected(); void _telnetHandle(); void _telnetCommand(char * commandLine); - char * _telnet_readWord(); + char * _telnet_readWord(bool allow_all_chars); void _telnet_setup(); char _command[TELNET_MAX_COMMAND_LENGTH]; // the input command from either Serial or Telnet command_t * _helpProjectCmds; // Help of commands setted by project @@ -250,8 +250,7 @@ class MyESP { void _consoleShowHelp(); telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands telnet_callback_f _telnet_callback; // callback for connect/disconnect - void _changeSetting(uint8_t wc, const char * setting, const char * value); - void _changeSetting2(const char * setting, const char * value1, const char * value2); + bool _changeSetting(uint8_t wc, const char * setting, const char * value); // fs void _fs_setup(); From ddcee120ed9029c9acd86ad0cc7f59a42531c57a Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 19 Mar 2019 21:34:13 +0100 Subject: [PATCH 33/59] added SM10 support --- src/ems-esp.ino | 14 ++++++++ src/ems.cpp | 82 +++++++++++++++++++++++++++++++++++++++++++++-- src/ems.h | 24 ++++++++++++++ src/ems_devices.h | 29 +++++++++++------ src/version.h | 2 +- 5 files changed, 138 insertions(+), 13 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 330fe775e..b663813ed 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -389,6 +389,19 @@ void showInfo() { myDebug(""); // newline + // For SM10 Solar Module + if (EMS_Other.SM10) { + _renderIntValue("SM10 modulation pump", "%", EMS_Other.SM10modulationSolarPump); + _renderFloatValue("SM10 collector temperature", "C", EMS_Other.SM10collectorTemp); + _renderBoolValue("SM10 pump", EMS_Other.SM10pumpOn); + myDebug(" SM10 uptime: %d days %d hours %d minutes", + EMS_Other.SM10Uptime / 1440, + (EMS_Other.SM10Uptime % 1440) / 60, + EMS_Other.SM10Uptime % 60); + } + + myDebug(""); // newline + // Thermostat stats if (ems_getThermostatEnabled()) { myDebug("%sThermostat stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); @@ -1217,6 +1230,7 @@ void do_regularUpdates() { myDebugLog("Calling scheduled data refresh from EMS devices.."); ems_getThermostatValues(); ems_getBoilerValues(); + ems_getOtherValues(); } } diff --git a/src/ems.cpp b/src/ems.cpp index 1db816e0b..46cc614f1 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -34,6 +34,7 @@ void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length); void _process_UBATotalUptimeMessage(uint8_t type, uint8_t * data, uint8_t length); void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length); void _process_SetPoints(uint8_t type, uint8_t * data, uint8_t length); +void _process_SM10Monitor(uint8_t type, uint8_t * data, uint8_t length); // Common for most thermostats void _process_RCTime(uint8_t type, uint8_t * data, uint8_t length); @@ -77,6 +78,9 @@ const _EMS_Type EMS_Types[] = { {EMS_MODEL_UBA, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", _process_UBAParametersMessage}, {EMS_MODEL_UBA, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, + // Other devices + {EMS_MODEL_OTHER, EMS_TYPE_SM10Monitor, "SM10Monitor", _process_SM10Monitor}, + // RC10 {EMS_MODEL_RC10, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, {EMS_MODEL_RC10, EMS_TYPE_RC10Set, "RC10Set", _process_RC10Set}, @@ -115,17 +119,18 @@ const _EMS_Type EMS_Types[] = { {EMS_MODEL_EASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, {EMS_MODEL_BOSCHEASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, - }; // calculate sizes of arrays at compile uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types -uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of models +uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of boiler models +uint8_t _Other_Types_max = ArraySize(Other_Types); // number of other ems devices uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types // these structs contain the data we store from the Boiler and Thermostat _EMS_Boiler EMS_Boiler; _EMS_Thermostat EMS_Thermostat; +_EMS_Other EMS_Other; // CRC lookup table with poly 12 for faster checking const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, @@ -229,6 +234,12 @@ void ems_init() { EMS_Boiler.pump_mod_max = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation max. power EMS_Boiler.pump_mod_min = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation min. power + // Other EMS devices values + EMS_Other.SM10collectorTemp = EMS_VALUE_FLOAT_NOTSET; // collector temp from SM10 + EMS_Other.SM10modulationSolarPump = EMS_VALUE_INT_NOTSET; // modulation solar pump + EMS_Other.SM10pumpOn = EMS_VALUE_INT_NOTSET; // SM10 pump on/off + EMS_Other.SM10Uptime = EMS_VALUE_LONG_NOTSET; // SM10 uptime + // calculated values EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off EMS_Boiler.heatingActive = EMS_VALUE_INT_NOTSET; // Central heating is on/off @@ -242,6 +253,9 @@ void ems_init() { EMS_Thermostat.product_id = 0; strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version)); + // set other types + EMS_Other.SM10 = false; + // default logging is none ems_setLogging(EMS_SYS_LOGGING_DEFAULT); } @@ -1219,6 +1233,33 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { // get Thermostat values (if supported) ems_getThermostatValues(); } + return; + } + + // finally look for the other devices + i = 0; + while (i < _Other_Types_max) { + if (Other_Types[i].product_id == product_id) { + typeFound = true; // we have a matching product id. i is the index. + break; + } + i++; + } + + if (typeFound) { + // its a boiler + myDebug("Device found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + Other_Types[i].model_string, + Other_Types[i].type_id, + product_id, + version); + + // see if this is a Solar Module SM10 // TODO: tidy up + if (Other_Types[i].type_id == EMS_ID_SM10) { + EMS_Other.SM10 = true; // we have detected a SM10 + } + + return; } else { myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", type, product_id, version); } @@ -1280,6 +1321,18 @@ void _ems_setThermostatModel(uint8_t thermostat_modelid) { EMS_Thermostat.write_supported = thermostat_type->write_supported; } +/* + * SM10Monitor - type 0x97 + */ +void _process_SM10Monitor(uint8_t type, uint8_t * data, uint8_t length) { + // TODO: polish off + EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 + EMS_Other.SM10modulationSolarPump = data[4]; // modulation solar pump + EMS_Other.SM10pumpOn = bitRead(data[6], 1); // SM10 pump on/off + EMS_Other.SM10Uptime = _toLong(8, data); // SM10 uptime +} + + /** * UBASetPoint 0x1A */ @@ -1409,6 +1462,15 @@ void ems_getBoilerValues() { ems_doReadCommand(EMS_TYPE_UBATotalUptimeMessage, EMS_Boiler.type_id); // get uptime from boiler } +/* + * Get other values from EMS devices + */ +void ems_getOtherValues() { + if (EMS_Other.SM10) { + ems_doReadCommand(EMS_TYPE_SM10Monitor, EMS_ID_SM10); // fetch all from SM10Monitor, e.g. 0B B0 97 00 16 + } +} + /** * returns current thermostat type as a string */ @@ -1491,7 +1553,7 @@ char * ems_getBoilerDescription(char * buffer) { void ems_scanDevices() { myDebug("Started scan of EMS bus for known devices"); - std::list Device_Ids; // new list + std::list Device_Ids; // create a new list // copy over boilers for (_Boiler_Type bt : Boiler_Types) { @@ -1502,6 +1564,12 @@ void ems_scanDevices() { for (_Thermostat_Type tt : Thermostat_Types) { Device_Ids.push_back(tt.type_id); } + + // copy over others + for (_Other_Type ot : Other_Types) { + Device_Ids.push_back(ot.type_id); + } + // remove duplicates and reserved IDs (like our own device) Device_Ids.sort(); Device_Ids.unique(); @@ -1533,6 +1601,14 @@ void ems_printAllTypes() { } } + myDebug("\nThese telegram type IDs are recognized for other EMS devices:"); + + for (i = 0; i < _EMS_Types_max; i++) { + if (EMS_Types[i].model_id == EMS_MODEL_OTHER) { + myDebug(" type %02X (%s)", EMS_Types[i].type, EMS_Types[i].typeString); + } + } + myDebug("\nThese %d thermostats models are supported:", _Thermostat_Types_max); for (i = 0; i < _Thermostat_Types_max; i++) { myDebug(" %s, type ID:0x%02X Product ID:%d Read/Write support:%c%c", diff --git a/src/ems.h b/src/ems.h index 8fb3caa94..581d297f5 100644 --- a/src/ems.h +++ b/src/ems.h @@ -17,6 +17,8 @@ #define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as the "Service Key" #define EMS_ID_DEFAULT_BOILER 0x08 +#define EMS_ID_SM10 0x30 + #define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC // max length of a telegram, including CRC, for Rx and Tx. Data size is 32, so reserving 40 to be safe @@ -145,6 +147,13 @@ typedef struct { char model_string[50]; } _Boiler_Type; +typedef struct { + uint8_t model_id; + uint8_t product_id; + uint8_t type_id; + char model_string[50]; +} _Other_Type; + // Definition for thermostat type typedef struct { uint8_t model_id; @@ -215,6 +224,18 @@ typedef struct { // UBAParameterWW uint8_t product_id; } _EMS_Boiler; +/* + * Telegram package defintions for Other EMS devices + */ +typedef struct { + // SM10 Solar Module - SM10Monitor + bool SM10; // set true if there is a SM10 available + float SM10collectorTemp; // collector temp from SM10 + uint8_t SM10modulationSolarPump; // modulation solar pump + uint8_t SM10pumpOn; // SM10 pump on/off + uint32_t SM10Uptime; // SM10 uptime +} _EMS_Other; + // Thermostat data typedef struct { uint8_t type_id; // the type ID of the thermostat @@ -267,6 +288,7 @@ void ems_setTxDisabled(bool b); void ems_getThermostatValues(); void ems_getBoilerValues(); +void ems_getOtherValues(); bool ems_getPoll(); bool ems_getTxEnabled(); bool ems_getThermostatEnabled(); @@ -300,3 +322,5 @@ void _removeTxQueue(); extern _EMS_Sys_Status EMS_Sys_Status; extern _EMS_Boiler EMS_Boiler; extern _EMS_Thermostat EMS_Thermostat; +extern _EMS_Other EMS_Other; + diff --git a/src/ems_devices.h b/src/ems_devices.h index 9a27f957a..7159071e7 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -39,6 +39,9 @@ #define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco #define EMS_VALUE_UBAParameterWW_wwComfort_Intelligent 0xEC // the value for intelligent +// Other +#define EMS_TYPE_SM10Monitor 0x97 // SM10Monitor + /* * Thermostats... */ @@ -93,7 +96,10 @@ typedef enum { // generic ID for the boiler EMS_MODEL_UBA, - // thermostats + // generic ID for all the other weird devices + EMS_MODEL_OTHER, + + // and finaly the thermostats EMS_MODEL_ES73, EMS_MODEL_RC10, EMS_MODEL_RC20, @@ -117,14 +123,19 @@ const _Boiler_Type Boiler_Types[] = { {EMS_MODEL_UBA, 115, 0x08, "Nefit Topline Compact"}, {EMS_MODEL_UBA, 203, 0x08, "Buderus Logamax U122"}, {EMS_MODEL_UBA, 64, 0x08, "Sieger BK15 Boiler/Nefit Smartline"}, - {EMS_MODEL_UBA, 190, 0x09, "BC10 Base Controller"}, - {EMS_MODEL_UBA, 114, 0x09, "BC10 Base Controller"}, - {EMS_MODEL_UBA, 125, 0x09, "BC25 Base Controller"}, - {EMS_MODEL_UBA, 205, 0x02, "Nefit Moduline Easy Connect"}, - {EMS_MODEL_UBA, 68, 0x09, "RFM20 Receiver"}, - {EMS_MODEL_UBA, 95, 0x08, "Bosch Condens 2500"}, - {EMS_MODEL_UBA, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! - {EMS_MODEL_UBA, 250, 0x11, "WM10 Switch Module"}, // warning, fake product id! + {EMS_MODEL_UBA, 95, 0x08, "Bosch Condens 2500"} + +}; + +// Other EMS devices which are not considered boilers or thermostats +const _Other_Type Other_Types[] = {{EMS_MODEL_OTHER, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! + {EMS_MODEL_OTHER, 250, 0x11, "WM10 Switch Module"}, // warning, fake product id! + {EMS_MODEL_OTHER, 68, 0x09, "RFM20 Receiver"}, + {EMS_MODEL_OTHER, 190, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_OTHER, 114, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_OTHER, 125, 0x09, "BC25 Base Controller"}, + {EMS_MODEL_OTHER, 205, 0x02, "Nefit Moduline Easy Connect"}, + {EMS_MODEL_OTHER, 73, 0x02, "SM10 Solar Module"} }; diff --git a/src/version.h b/src/version.h index 366b539e4..c1d9828b5 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.6.0b3" +#define APP_VERSION "1.6.0b4" #define APP_HOSTNAME "ems-esp" From a13e7f177f761918a108a7bca4e5d54bef89cd5d Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 19 Mar 2019 23:50:19 +0100 Subject: [PATCH 34/59] optimizations --- lib/MyESP/MyESP.cpp | 15 ++++++--------- src/ems-esp.ino | 2 +- src/ems.cpp | 21 +++++++++------------ src/ems.h | 4 +--- src/ems_devices.h | 20 +++++++++++--------- 5 files changed, 28 insertions(+), 34 deletions(-) diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index a42996251..bddcf6e45 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -1191,6 +1191,7 @@ bool MyESP::_fs_loadConfig() { bool MyESP::fs_saveConfig() { bool ok = true; + // call any custom functions before handling SPIFFS if (_ota_pre_callback) { (_ota_pre_callback)(); } @@ -1211,35 +1212,31 @@ bool MyESP::fs_saveConfig() { // if file exists, remove it just to be safe if (SPIFFS.exists(MYEMS_CONFIG_FILE)) { - // delete it SPIFFS.remove(MYEMS_CONFIG_FILE); } + // open for writing File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); if (!configFile) { myDebug_P(PSTR("[FS] Failed to open config file for writing")); - ok = false; + return false; } - /* - if (ok) { - myDebug_P(PSTR("[FS] Writing config file")); - } - */ // Serialize JSON to file if (serializeJson(json, configFile) == 0) { - myDebug_P(PSTR("[FS] Failed to write to file")); + myDebug_P(PSTR("[FS] Failed to write config file")); ok = false; } configFile.close(); + // call any custom functions before handling SPIFFS if (_ota_post_callback) { (_ota_post_callback)(); } - return ok; + return ok; // it worked } // init the SPIFF file system and load the config diff --git a/src/ems-esp.ino b/src/ems-esp.ino index b663813ed..e4d1cd974 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -1354,7 +1354,7 @@ void setup() { MQTT_WILL_OFFLINE_PAYLOAD, MQTTCallback); - // OTA callback which is called when OTA is starting + // OTA callback which is called when OTA is starting and stopping myESP.setOTA(OTACallback_pre, OTACallback_post); // custom settings in SPIFFS diff --git a/src/ems.cpp b/src/ems.cpp index 46cc614f1..666b80454 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -781,7 +781,7 @@ void _ems_processTelegram(_EMS_RxTelegram EMS_RxTelegram) { // call callback function to process it // as we only handle complete telegrams (not partial) check that the offset is 0 if (offset == EMS_ID_NONE) { - (void)EMS_Types[i].processType_cb(type, data, EMS_RxTelegram.length - 5); + (void)EMS_Types[i].processType_cb(src, data, EMS_RxTelegram.length - 5); } } } @@ -1247,7 +1247,6 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { } if (typeFound) { - // its a boiler myDebug("Device found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", Other_Types[i].model_string, Other_Types[i].type_id, @@ -1272,6 +1271,9 @@ void ems_discoverModels() { // boiler ems_doReadCommand(EMS_TYPE_Version, EMS_Boiler.type_id); // get version details of boiler + // solar module + ems_doReadCommand(EMS_TYPE_Version, EMS_ID_SM10); // check if there is Solar Module available + // thermostat // if it hasn't been set, auto discover it if (EMS_Thermostat.type_id == EMS_ID_NONE) { @@ -1588,23 +1590,18 @@ void ems_printAllTypes() { uint8_t i; myDebug("\nThese %d boiler type devices are in the library:", _Boiler_Types_max); - for (i = 0; i < _Boiler_Types_max; i++) { myDebug(" %s, type ID:0x%02X Product ID:%d", Boiler_Types[i].model_string, Boiler_Types[i].type_id, Boiler_Types[i].product_id); } - myDebug("\nThese telegram type IDs are recognized for the selected boiler:"); - - for (i = 0; i < _EMS_Types_max; i++) { - if ((EMS_Types[i].model_id == EMS_MODEL_ALL) || (EMS_Types[i].model_id == EMS_MODEL_UBA)) { - myDebug(" type %02X (%s)", EMS_Types[i].type, EMS_Types[i].typeString); - } + myDebug("\nThese %d EMS devices are in the library:", _Other_Types_max); + for (i = 0; i < _Other_Types_max; i++) { + myDebug(" %s, type ID:0x%02X Product ID:%d", Other_Types[i].model_string, Other_Types[i].type_id, Other_Types[i].product_id); } - myDebug("\nThese telegram type IDs are recognized for other EMS devices:"); - + myDebug("\nThese telegram type IDs are recognized for the selected boiler:"); for (i = 0; i < _EMS_Types_max; i++) { - if (EMS_Types[i].model_id == EMS_MODEL_OTHER) { + if ((EMS_Types[i].model_id == EMS_MODEL_ALL) || (EMS_Types[i].model_id == EMS_MODEL_UBA)) { myDebug(" type %02X (%s)", EMS_Types[i].type, EMS_Types[i].typeString); } } diff --git a/src/ems.h b/src/ems.h index 581d297f5..04db7eaca 100644 --- a/src/ems.h +++ b/src/ems.h @@ -16,8 +16,7 @@ #define EMS_ID_NONE 0x00 // Fixed - used as a dest in broadcast messages and empty type IDs #define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as the "Service Key" #define EMS_ID_DEFAULT_BOILER 0x08 - -#define EMS_ID_SM10 0x30 +#define EMS_ID_SM10 0x30 // Solar Module SM10 #define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC @@ -323,4 +322,3 @@ extern _EMS_Sys_Status EMS_Sys_Status; extern _EMS_Boiler EMS_Boiler; extern _EMS_Thermostat EMS_Thermostat; extern _EMS_Other EMS_Other; - diff --git a/src/ems_devices.h b/src/ems_devices.h index 7159071e7..84f650a3a 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -118,7 +118,7 @@ typedef enum { // format is MODEL_ID, PRODUCT ID, TYPE_ID, DESCRIPTION const _Boiler_Type Boiler_Types[] = { - {EMS_MODEL_UBA, 72, 0x08, "MC10"}, + {EMS_MODEL_UBA, 72, 0x08, "MC10 Module"}, {EMS_MODEL_UBA, 123, 0x08, "Buderus GB172/Nefit Trendline"}, {EMS_MODEL_UBA, 115, 0x08, "Nefit Topline Compact"}, {EMS_MODEL_UBA, 203, 0x08, "Buderus Logamax U122"}, @@ -128,14 +128,16 @@ const _Boiler_Type Boiler_Types[] = { }; // Other EMS devices which are not considered boilers or thermostats -const _Other_Type Other_Types[] = {{EMS_MODEL_OTHER, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! - {EMS_MODEL_OTHER, 250, 0x11, "WM10 Switch Module"}, // warning, fake product id! - {EMS_MODEL_OTHER, 68, 0x09, "RFM20 Receiver"}, - {EMS_MODEL_OTHER, 190, 0x09, "BC10 Base Controller"}, - {EMS_MODEL_OTHER, 114, 0x09, "BC10 Base Controller"}, - {EMS_MODEL_OTHER, 125, 0x09, "BC25 Base Controller"}, - {EMS_MODEL_OTHER, 205, 0x02, "Nefit Moduline Easy Connect"}, - {EMS_MODEL_OTHER, 73, 0x02, "SM10 Solar Module"} +const _Other_Type Other_Types[] = { + + {EMS_MODEL_OTHER, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! + {EMS_MODEL_OTHER, 250, 0x11, "WM10 Switch Module"}, // warning, fake product id! + {EMS_MODEL_OTHER, 68, 0x09, "RFM20 Receiver"}, + {EMS_MODEL_OTHER, 190, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_OTHER, 114, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_OTHER, 125, 0x09, "BC25 Base Controller"}, + {EMS_MODEL_OTHER, 205, 0x02, "Nefit Moduline Easy Connect"}, + {EMS_MODEL_OTHER, 73, 0x02, "SM10 Solar Module"} }; From 159a36e7de480ec4d327787eb9e58dccee4ac1b8 Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 20 Mar 2019 09:26:14 +0100 Subject: [PATCH 35/59] changes to settings --- src/ems-esp.ino | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index e4d1cd974..3a2570d60 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -32,7 +32,7 @@ DS18 ds18; #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) // timers, all values are in seconds -#define DEFAULT_PUBLISHVALUES_TIME 120 // every 2 minutes publish MQTT values, including Dallas sensors +#define DEFAULT_PUBLISHWAIT 120 // every 2 minutes publish MQTT values, including Dallas sensors Ticker publishValuesTimer; Ticker publishSensorValuesTimer; @@ -68,7 +68,7 @@ typedef struct { bool shower_alert; // true if we want the alert of cold water bool led; // LED on/off bool silent_mode; // stop automatic Tx on/off - uint16_t publish_time; // frequency of MQTT publish in seconds + uint16_t publish_wait; // frequency of MQTT publish in seconds uint8_t led_gpio; uint8_t dallas_gpio; uint8_t dallas_parasite; @@ -93,10 +93,11 @@ command_t PROGMEM project_cmds[] = { {true, "silent_mode ", "when on all automatic Tx is disabled"}, {true, "shower_timer ", "notify via MQTT all shower durations"}, {true, "shower_alert ", "send a warning of cold water after shower time is exceeded"}, + {true, "publish_wait ", "set frequency for publishing to MQTT"}, + {false, "info", "show data captured on the EMS bus"}, {false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, {false, "publish", "publish all values to MQTT"}, - {false, "publish_time ", "set frequency for MQTT publishing of values"}, {false, "types", "list supported EMS telegram type IDs"}, {false, "queue", "show current Tx queue"}, {false, "autodetect", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, @@ -696,18 +697,14 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { ems_setTxDisabled(EMSESP_Status.silent_mode); // shower_timer - if (!(EMSESP_Status.shower_timer = json["shower_timer"])) { - EMSESP_Status.shower_timer = false; // default value - } + EMSESP_Status.shower_timer = json["shower_timer"]; // shower_alert - if (!(EMSESP_Status.shower_alert = json["shower_alert"])) { - EMSESP_Status.shower_alert = false; // default value - } + EMSESP_Status.shower_alert = json["shower_alert"]; - // publish_time - if (!(EMSESP_Status.publish_time = json["publish_time"])) { - EMSESP_Status.publish_time = DEFAULT_PUBLISHVALUES_TIME; // default value + // publish_wait + if (!(EMSESP_Status.publish_wait = json["publish_wait"])) { + EMSESP_Status.publish_wait = DEFAULT_PUBLISHWAIT; // default value } return recreate_config; // return false if some settings are missing and we need to rebuild the file @@ -723,7 +720,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { json["silent_mode"] = EMSESP_Status.silent_mode; json["shower_timer"] = EMSESP_Status.shower_timer; json["shower_alert"] = EMSESP_Status.shower_alert; - json["publish_time"] = EMSESP_Status.publish_time; + json["publish_wait"] = EMSESP_Status.publish_wait; return true; } @@ -836,9 +833,9 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } } - // publish_time - if ((strcmp(setting, "publish_time") == 0) && (wc == 2)) { - EMSESP_Status.publish_time = atoi(value); + // publish_wait + if ((strcmp(setting, "publish_wait") == 0) && (wc == 2)) { + EMSESP_Status.publish_wait = atoi(value); ok = true; } } @@ -866,7 +863,7 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug(" silent_mode=%s", EMSESP_Status.silent_mode ? "on" : "off"); myDebug(" shower_timer=%s", EMSESP_Status.shower_timer ? "on" : "off"); myDebug(" shower_alert=%s", EMSESP_Status.shower_alert ? "on" : "off"); - myDebug(" publish_time=%d", EMSESP_Status.publish_time); + myDebug(" publish_wait=%d", EMSESP_Status.publish_wait); } return ok; @@ -1164,7 +1161,7 @@ void initEMSESP() { EMSESP_Status.shower_alert = false; EMSESP_Status.led = true; // LED is on by default EMSESP_Status.silent_mode = false; - EMSESP_Status.publish_time = DEFAULT_PUBLISHVALUES_TIME; + EMSESP_Status.publish_wait = DEFAULT_PUBLISHWAIT; EMSESP_Status.timestamp = millis(); EMSESP_Status.dallas_sensors = 0; @@ -1367,8 +1364,8 @@ void setup() { // enable regular checks if not in test mode if (!EMSESP_Status.silent_mode) { - publishValuesTimer.attach(EMSESP_Status.publish_time, do_publishValues); // post MQTT EMS values - publishSensorValuesTimer.attach(EMSESP_Status.publish_time, do_publishSensorValues); // post MQTT sensor values + publishValuesTimer.attach(EMSESP_Status.publish_wait, do_publishValues); // post MQTT EMS values + publishSensorValuesTimer.attach(EMSESP_Status.publish_wait, do_publishSensorValues); // post MQTT sensor values regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS } From f7575bf648e728336283ac3064ccfe20a9c8f838 Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 20 Mar 2019 12:30:55 +0100 Subject: [PATCH 36/59] fixes for SM10 detection --- src/ems.cpp | 5 +++-- src/ems_devices.h | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index 666b80454..dc0078add 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -1236,7 +1236,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { return; } - // finally look for the other devices + // finally look for the other EMS devices i = 0; while (i < _Other_Types_max) { if (Other_Types[i].product_id == product_id) { @@ -1253,9 +1253,10 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { product_id, version); - // see if this is a Solar Module SM10 // TODO: tidy up + // see if this is a Solar Module SM10 if (Other_Types[i].type_id == EMS_ID_SM10) { EMS_Other.SM10 = true; // we have detected a SM10 + myDebug("SM10 Solar Module support enabled."); } return; diff --git a/src/ems_devices.h b/src/ems_devices.h index 84f650a3a..7bfa1da23 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -137,7 +137,7 @@ const _Other_Type Other_Types[] = { {EMS_MODEL_OTHER, 114, 0x09, "BC10 Base Controller"}, {EMS_MODEL_OTHER, 125, 0x09, "BC25 Base Controller"}, {EMS_MODEL_OTHER, 205, 0x02, "Nefit Moduline Easy Connect"}, - {EMS_MODEL_OTHER, 73, 0x02, "SM10 Solar Module"} + {EMS_MODEL_OTHER, 73, EMS_ID_SM10, "SM10 Solar Module"} }; From a72276d24477d5f69d27bb0dc2a772641a988d78 Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 20 Mar 2019 13:10:21 +0100 Subject: [PATCH 37/59] removed Arduino IDE (because its nasty) --- README.md | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 586fa3e8b..d62cd38f9 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,9 @@ Every telegram sent is echo'd back to Rx, along the same Bus used for all Rx/Tx `ems_devices.h` has all the configuration for the known EMS devices currently supported. -`MyESP.cpp` is my custom library to handle WiFi, MQTT and Telnet. Uses a modified version of [TelnetSpy](https://github.com/yasheena/telnetspy) +`MyESP.cpp` is my custom library to handle WiFi, MQTT and Telnet. Uses a modified version of [TelnetSpy](https://github.com/yasheena/telnetspy). + +`ds18.*` are the Dallas libraries for any external temperature sensors. ### Special EMS Types @@ -228,18 +230,15 @@ In `ems.cpp` you can add scheduled calls to specific EMS types in the functions I am still working on adding more support to known thermostats. Any contributions here are welcome. The know types are listed in `ems_devices.h` and include -- RC20 and RC30, both are fully supported -- RC10 support is being added +- RC10, RC20 and RC30 are fully supported - RC35 with support for the 1st heating circuit (HC1) -- TC100/TC200/Easy but only with support for reading the temperatures. There seems to be no way to set settings using EMS bus messages that I know of. One option is to send XMPP messages but a special server is needed and out of scope for this project. +- TC100/TC200/Easy but only with support for *reading* the temperature values. There seems to be no way to set settings using EMS bus messages that I know of. One option is to send XMPP messages but a special server is needed and out of scope for this project. ### Customizing The Code -- To configure for your thermostat and specific boiler settings, modify `my_config.h`. Here you can - - set flags for enabled/disabling functionality such as `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`. - - Set WIFI and MQTT settings. The values can also be set from the telnet command menu using the **set** command. -- To add new handlers for EMS data types, first create a callback function and add to the `EMS_Types` array at the top of the file `ems.cpp` and modify `ems.h` -- To add new devices modify `ems_devices.h` +- To configure for your thermostat and specific boiler settings, modify `my_config.h`. +- Most values can also be set from the telnet command menu using the **set** command. +- To add new handlers for EMS data types, first create a callback function and add to the `EMS_Types` array at the top of the file `ems.cpp` and modify `ems.h`. Also add to `ems_devices.h`. ### Using MQTT @@ -302,6 +301,7 @@ Make sure Python 2.7 is installed, then... % pip install -U platformio % sudo platformio upgrade % platformio platform update +% platformio lib upgrade % git clone https://github.com/proddy/EMS-ESP.git % cd EMS-ESP @@ -312,17 +312,6 @@ edit `platformio.ini` to set `env_default` to your board type, then % platformio run -t upload ``` -### Building Using Arduino IDE - -Porting to the Arduino IDE can be a little tricky but it did it once. Something along these lines: - -- Add the ESP8266 boards (from Preferences add Additional Board URL `http://arduino.esp8266.com/stable/package_esp8266com_index.json`) -- Go to Boards Manager and install ESP8266 2.4.x platform. Make sure your board supports SPIFFS. -- Select your ESP8266 from Tools->Boards and the correct port with Tools->Port -- From the Library Manager install the needed libraries from platformio.ini. Note make sure you pick ArduinoJson v5 (5.13.4 and above) and not v6. See https://arduinojson.org/v5/doc/ -- Put all the files in a single sketch folder -- cross your fingers and hit CTRL-R to compile - ## Using the Pre-built Firmware pre-baked firmware for the Wemos D1 mini is available in the GitHub [releases](https://github.com/proddy/EMS-ESP/releases) which you can upload yourself using the [esptool](https://github.com/espressif/esptool) bootloader like `esptool.py -p write_flash 0x00000 `. Here's how to set it up on Windows: From 5079a3914f4df4f8568100b2b4af9a632086d0d8 Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 20 Mar 2019 18:10:18 +0100 Subject: [PATCH 38/59] SM10 changes --- src/ems-esp.ino | 8 ++------ src/ems.cpp | 19 +++++++++++-------- src/ems.h | 13 ++++++------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 3a2570d60..395fb186f 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -392,13 +392,9 @@ void showInfo() { // For SM10 Solar Module if (EMS_Other.SM10) { - _renderIntValue("SM10 modulation pump", "%", EMS_Other.SM10modulationSolarPump); _renderFloatValue("SM10 collector temperature", "C", EMS_Other.SM10collectorTemp); - _renderBoolValue("SM10 pump", EMS_Other.SM10pumpOn); - myDebug(" SM10 uptime: %d days %d hours %d minutes", - EMS_Other.SM10Uptime / 1440, - (EMS_Other.SM10Uptime % 1440) / 60, - EMS_Other.SM10Uptime % 60); + _renderFloatValue("SM10 bottom temperature", "C", EMS_Other.SM10bottomTemp); + _renderIntValue("SM10 pump", "%", EMS_Other.SM10pumpModulation); } myDebug(""); // newline diff --git a/src/ems.cpp b/src/ems.cpp index dc0078add..3ac573d70 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -235,10 +235,9 @@ void ems_init() { EMS_Boiler.pump_mod_min = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation min. power // Other EMS devices values - EMS_Other.SM10collectorTemp = EMS_VALUE_FLOAT_NOTSET; // collector temp from SM10 - EMS_Other.SM10modulationSolarPump = EMS_VALUE_INT_NOTSET; // modulation solar pump - EMS_Other.SM10pumpOn = EMS_VALUE_INT_NOTSET; // SM10 pump on/off - EMS_Other.SM10Uptime = EMS_VALUE_LONG_NOTSET; // SM10 uptime + EMS_Other.SM10collectorTemp = EMS_VALUE_FLOAT_NOTSET; // collector temp from SM10 + EMS_Other.SM10bottomTemp = EMS_VALUE_FLOAT_NOTSET; // bottom temp from SM10 + EMS_Other.SM10pumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10 // calculated values EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off @@ -712,6 +711,8 @@ void _ems_processTelegram(_EMS_RxTelegram EMS_RxTelegram) { strlcpy(output_str, "Boiler", sizeof(output_str)); } else if (src == EMS_Thermostat.type_id) { strlcpy(output_str, "Thermostat", sizeof(output_str)); + } else if (src == EMS_ID_SM10) { + strlcpy(output_str, "SM10", sizeof(output_str)); } else { strlcpy(output_str, "0x", sizeof(output_str)); strlcat(output_str, _hextoa(src, buffer), sizeof(output_str)); @@ -729,6 +730,9 @@ void _ems_processTelegram(_EMS_RxTelegram EMS_RxTelegram) { } else if (dest == EMS_Boiler.type_id) { strlcat(output_str, "Boiler", sizeof(output_str)); strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + } else if (dest == EMS_ID_SM10) { + strlcat(output_str, "SM10", sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); } else if (dest == EMS_Thermostat.type_id) { strlcat(output_str, "Thermostat", sizeof(output_str)); strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); @@ -1329,10 +1333,9 @@ void _ems_setThermostatModel(uint8_t thermostat_modelid) { */ void _process_SM10Monitor(uint8_t type, uint8_t * data, uint8_t length) { // TODO: polish off - EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 - EMS_Other.SM10modulationSolarPump = data[4]; // modulation solar pump - EMS_Other.SM10pumpOn = bitRead(data[6], 1); // SM10 pump on/off - EMS_Other.SM10Uptime = _toLong(8, data); // SM10 uptime + EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 + EMS_Other.SM10bottomTemp = _toFloat(5, data); // bottom temp from SM10 + EMS_Other.SM10pumpModulation = data[4]; // modulation solar pump } diff --git a/src/ems.h b/src/ems.h index 04db7eaca..90027f4da 100644 --- a/src/ems.h +++ b/src/ems.h @@ -20,8 +20,8 @@ #define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC -// max length of a telegram, including CRC, for Rx and Tx. Data size is 32, so reserving 40 to be safe -#define EMS_MAX_TELEGRAM_LENGTH 40 +// max length of a telegram, including CRC, for Rx and Tx. +#define EMS_MAX_TELEGRAM_LENGTH 32 // default values #define EMS_VALUE_INT_ON 1 // boolean true @@ -228,11 +228,10 @@ typedef struct { // UBAParameterWW */ typedef struct { // SM10 Solar Module - SM10Monitor - bool SM10; // set true if there is a SM10 available - float SM10collectorTemp; // collector temp from SM10 - uint8_t SM10modulationSolarPump; // modulation solar pump - uint8_t SM10pumpOn; // SM10 pump on/off - uint32_t SM10Uptime; // SM10 uptime + bool SM10; // set true if there is a SM10 available + float SM10collectorTemp; // collector temp from SM10 + float SM10bottomTemp; // bottom temp from SM10 + uint8_t SM10pumpModulation; // modulation solar pump } _EMS_Other; // Thermostat data From 7eaab743bf1111030817be877b4d4ba7bd626cb4 Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 20 Mar 2019 21:55:40 +0100 Subject: [PATCH 39/59] mem optimizatons --- CHANGELOG.md | 14 +-- lib/MyESP/MyESP.cpp | 1 + platformio.ini-example | 2 +- src/ems-esp.ino | 5 +- src/ems.cpp | 219 ++++++++++++++++++++--------------------- src/ems.h | 6 +- src/emsuart.cpp | 2 +- 7 files changed, 124 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5fbf823e..e7c08e3b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.6.0 dev] 2019-03-19 +## [1.6.0 dev] 2019-03-20 ### Added -- system command to show ESP stats -- crash command to see stack of last system crash, with .py files to track stack dump -- publish dallas external temp sensors to MQTT (Thanks @JewelZB) +- system command to show ESP8266 stats +- crash command to see stack of last system crash, with .py files to track stack dump (compile with -DCRASH) +- publish dallas external temp sensors to MQTT (thanks @JewelZB) - shower timer and shower alert options available via set commands -- Added support for warm water modes Hot, Comfort and Intelligent (https://github.com/proddy/EMS-ESP/issues/67) +- added support for warm water modes Hot, Comfort and Intelligent (https://github.com/proddy/EMS-ESP/issues/67) +- added 'set publish_time' to set how often to publish MQTT +- support for SM10 Solar Module ### Fixed @@ -25,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - included various fixes and suggestions from @nomis -- upgraded MyESP library +- upgraded MyESP library with many optimizations - test_mode renamed to silent_mode - 'set wifi' replaced with 'set wifi_ssid and set wifi_password' to allow values with spaces diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index bddcf6e45..0072d3822 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -766,6 +766,7 @@ String MyESP::_getESPhostname() { } // returns build time as a String - copied for espurna. see (c) +// takes the time from the gcc during compilation String MyESP::_buildTime() { const char time_now[] = __TIME__; // hh:mm:ss unsigned int hour = atoi(&time_now[0]); diff --git a/platformio.ini-example b/platformio.ini-example index 7e74d40e4..d6416d4db 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -11,7 +11,7 @@ platform = ${common.platform_def} flash_mode = dout ; for production -build_flags = -Os -w +build_flags = -g -w ; for debug ; build_flags = -g -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 395fb186f..c2354ee04 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -110,10 +110,7 @@ command_t PROGMEM project_cmds[] = { {false, "boiler read ", "send read request to boiler"}, {false, "boiler wwtemp ", "set boiler warm water temperature"}, {false, "boiler tapwater ", "set boiler warm tap water on/off"}, - {false, "boiler comfort ", "set boiler warm water comfort setting"}, - {false, "startup", "send startup sequence to bus master - still experimental"} - -}; + {false, "boiler comfort ", "set boiler warm water comfort setting"}}; // store for overall system status _EMSESP_Status EMSESP_Status; diff --git a/src/ems.cpp b/src/ems.cpp index 3ac573d70..0ef2d6898 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -24,40 +24,40 @@ CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO // callbacks per type // generic -void _process_Version(uint8_t type, uint8_t * data, uint8_t length); +void _process_Version(uint8_t src, uint8_t * data, uint8_t length); // Boiler and Buderus devices -void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAMonitorSlow(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBATotalUptimeMessage(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length); -void _process_SetPoints(uint8_t type, uint8_t * data, uint8_t length); -void _process_SM10Monitor(uint8_t type, uint8_t * data, uint8_t length); +void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAMonitorSlow(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAMonitorWWMessage(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAParameterWW(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBATotalUptimeMessage(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAParametersMessage(uint8_t src, uint8_t * data, uint8_t length); +void _process_SetPoints(uint8_t src, uint8_t * data, uint8_t length); +void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length); // Common for most thermostats -void _process_RCTime(uint8_t type, uint8_t * data, uint8_t length); -void _process_RCOutdoorTempMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RCTime(uint8_t src, uint8_t * data, uint8_t length); +void _process_RCOutdoorTempMessage(uint8_t src, uint8_t * data, uint8_t length); // RC10 -void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC10Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC10StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // RC20 -void _process_RC20Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC20Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // RC30 -void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC30Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC30StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // RC35 -void _process_RC35Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC35Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC35StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // Easy -void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_EasyStatusMessage(uint8_t src, uint8_t * data, uint8_t length); /* * Recognized EMS types and the functions they call to process the telegrams @@ -424,24 +424,24 @@ char * _smallitoa3(uint16_t value, char * buffer) { * debug print a telegram to telnet/serial including the CRC * len is length in bytes including the CRC */ -void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram EMS_RxTelegram, const char * color) { +void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color) { if (EMS_Sys_Status.emsLogging <= EMS_SYS_LOGGING_BASIC) return; char output_str[200] = {0}; char buffer[16] = {0}; - uint8_t len = EMS_RxTelegram.length; - uint8_t * data = EMS_RxTelegram.telegram; + uint8_t len = EMS_RxTelegram->length; + uint8_t * data = EMS_RxTelegram->telegram; strlcpy(output_str, "(", sizeof(output_str)); strlcat(output_str, COLOR_CYAN, sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram.timestamp / 3600000) % 24), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 3600000) % 24), buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram.timestamp / 60000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 60000) % 60), buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram.timestamp / 1000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 1000) % 60), buffer), sizeof(output_str)); strlcat(output_str, ".", sizeof(output_str)); - strlcat(output_str, _smallitoa3(EMS_RxTelegram.timestamp % 1000, buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa3(EMS_RxTelegram->timestamp % 1000, buffer), sizeof(output_str)); strlcat(output_str, COLOR_RESET, sizeof(output_str)); strlcat(output_str, ") ", sizeof(output_str)); @@ -483,22 +483,23 @@ void _ems_sendTelegram() { // we don't remove from the queue yet _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); + // if there is no destination, also delete it from the queue + if (EMS_TxTelegram.dest == EMS_ID_NONE) { + EMS_TxQueue.shift(); // remove from queue + return; + } + + // if we're in raw mode just fire and forget if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_RAW) { EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC _EMS_RxTelegram EMS_RxTelegram; EMS_RxTelegram.length = EMS_TxTelegram.length; EMS_RxTelegram.telegram = EMS_TxTelegram.data; - EMS_RxTelegram.timestamp = millis(); // now - _debugPrintTelegram("Sending raw", EMS_RxTelegram, COLOR_CYAN); // always show - emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx - EMS_TxQueue.shift(); // remove from queue - return; - } - - // if there is no destination, also delete it from the queue - if (EMS_TxTelegram.dest == EMS_ID_NONE) { - EMS_TxQueue.shift(); // remove from queue + EMS_RxTelegram.timestamp = millis(); // now + _debugPrintTelegram("Sending raw", &EMS_RxTelegram, COLOR_CYAN); // always show + emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx + EMS_TxQueue.shift(); // remove from queue return; } @@ -540,7 +541,7 @@ void _ems_sendTelegram() { EMS_RxTelegram.length = EMS_TxTelegram.length; EMS_RxTelegram.telegram = EMS_TxTelegram.data; EMS_RxTelegram.timestamp = millis(); // now - _debugPrintTelegram(s, EMS_RxTelegram, COLOR_CYAN); + _debugPrintTelegram(s, &EMS_RxTelegram, COLOR_CYAN); } // send the telegram to the UART Tx @@ -605,7 +606,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // or either a return code like 0x01 or 0x04 from the last Write command // create the Rx package - _EMS_RxTelegram EMS_RxTelegram; + static _EMS_RxTelegram EMS_RxTelegram; EMS_RxTelegram.length = length; EMS_RxTelegram.telegram = telegram; EMS_RxTelegram.timestamp = millis(); @@ -651,7 +652,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // ignore anything that doesn't resemble a proper telegram package // minimal is 5 bytes, excluding CRC at the end if (length <= 4) { - //_debugPrintTelegram("Noisy data:", telegram, length, COLOR_RED); + //_debugPrintTelegram("Noisy data:", &EMS_RxTelegram COLOR_RED); return; } @@ -661,7 +662,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { if (telegram[length - 1] != crc) { EMS_Sys_Status.emxCrcErr++; if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - _debugPrintTelegram("Corrupt telegram:", EMS_RxTelegram, COLOR_RED); + _debugPrintTelegram("Corrupt telegram:", &EMS_RxTelegram, COLOR_RED); } return; } @@ -684,16 +685,16 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { EMS_Sys_Status.emsBusConnected = true; // now lets process it and see what to do next - _processType(EMS_RxTelegram); + _processType(&EMS_RxTelegram); } /** * print detailed telegram * and then call its callback if there is one defined */ -void _ems_processTelegram(_EMS_RxTelegram EMS_RxTelegram) { +void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { // header - uint8_t * telegram = EMS_RxTelegram.telegram; + uint8_t * telegram = EMS_RxTelegram->telegram; uint8_t src = telegram[0] & 0x7F; uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes uint8_t type = telegram[2]; @@ -785,7 +786,7 @@ void _ems_processTelegram(_EMS_RxTelegram EMS_RxTelegram) { // call callback function to process it // as we only handle complete telegrams (not partial) check that the offset is 0 if (offset == EMS_ID_NONE) { - (void)EMS_Types[i].processType_cb(src, data, EMS_RxTelegram.length - 5); + (void)EMS_Types[i].processType_cb(src, data, EMS_RxTelegram->length - 5); } } } @@ -806,9 +807,8 @@ void _removeTxQueue() { * length is only data bytes, excluding the BRK * We only remove from the Tx queue if the read or write was successful */ -void _processType(_EMS_RxTelegram EMS_RxTelegram) { - uint8_t * telegram = EMS_RxTelegram.telegram; - uint8_t length = EMS_RxTelegram.length; +void _processType(_EMS_RxTelegram * EMS_RxTelegram) { + uint8_t * telegram = EMS_RxTelegram->telegram; // header uint8_t src = telegram[0] & 0x7F; // removing 8th bit as we deal with both reads and writes here @@ -944,7 +944,7 @@ void _checkActive() { * UBAParameterWW - type 0x33 - warm water parameters * received only after requested (not broadcasted) */ -void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length) { +void _process_UBAParameterWW(uint8_t src, uint8_t * data, uint8_t length) { EMS_Boiler.wWActivated = (data[1] == 0xFF); // 0xFF means on EMS_Boiler.wWSelTemp = data[2]; EMS_Boiler.wWCircPump = (data[6] == 0xFF); // 0xFF means on @@ -958,7 +958,7 @@ void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length) { * UBATotalUptimeMessage - type 0x14 - total uptime * received only after requested (not broadcasted) */ -void _process_UBATotalUptimeMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_UBATotalUptimeMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Boiler.UBAuptime = _toLong(0, data); EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish } @@ -966,7 +966,7 @@ void _process_UBATotalUptimeMessage(uint8_t type, uint8_t * data, uint8_t length /* * UBAParametersMessage - type 0x16 */ -void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_UBAParametersMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Boiler.heating_temp = data[1]; EMS_Boiler.pump_mod_max = data[9]; EMS_Boiler.pump_mod_min = data[10]; @@ -976,7 +976,7 @@ void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length) * UBAMonitorWWMessage - type 0x34 - warm water monitor. 19 bytes long * received every 10 seconds */ -void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_UBAMonitorWWMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Boiler.wWCurTmp = _toFloat(1, data); EMS_Boiler.wWStarts = _toLong(13, data); EMS_Boiler.wWWorkM = _toLong(10, data); @@ -988,7 +988,7 @@ void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length) * UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long) * received every 10 seconds */ -void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { +void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length) { EMS_Boiler.selFlowTemp = data[0]; EMS_Boiler.curFlowTemp = _toFloat(1, data); EMS_Boiler.retTemp = _toFloat(13, data); @@ -1028,7 +1028,7 @@ void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { * UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long) * received every 60 seconds */ -void _process_UBAMonitorSlow(uint8_t type, uint8_t * data, uint8_t length) { +void _process_UBAMonitorSlow(uint8_t src, uint8_t * data, uint8_t length) { EMS_Boiler.extTemp = _toFloat(0, data); // 0x8000 if not available EMS_Boiler.boilTemp = _toFloat(2, data); // 0x8000 if not available EMS_Boiler.pumpMod = data[9]; @@ -1043,7 +1043,7 @@ void _process_UBAMonitorSlow(uint8_t type, uint8_t * data, uint8_t length) { * For reading the temp values only * received every 60 seconds */ -void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC10StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_setpoint]) / (float)2; EMS_Thermostat.curr_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_curr]) / (float)10; @@ -1055,7 +1055,7 @@ void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * For reading the temp values only * received every 60 seconds */ -void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC20StatusMessage_setpoint]) / (float)2; EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC20StatusMessage_curr, data); @@ -1067,7 +1067,7 @@ void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * For reading the temp values only * received every 60 seconds */ -void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC30StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC30StatusMessage_setpoint]) / (float)2; EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC30StatusMessage_curr, data); @@ -1079,7 +1079,7 @@ void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * For reading the temp values only * received every 60 seconds */ -void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC35StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC35StatusMessage_setpoint]) / (float)2; // check if temp sensor is unavailable @@ -1096,7 +1096,7 @@ void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long * The Easy has a digital precision of its floats to 2 decimal places, so values is divided by 100 */ -void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_EasyStatusMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.curr_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_curr] << 8) + data[9]))) / 100; EMS_Thermostat.setpoint_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_setpoint] << 8) + data[11]))) / 100; @@ -1107,7 +1107,7 @@ void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * type 0xB0 - for reading the mode from the RC10 thermostat (0x17) * received only after requested */ -void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC10Set(uint8_t src, uint8_t * data, uint8_t length) { // mode not implemented yet } @@ -1115,7 +1115,7 @@ void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length) { * type 0xA8 - for reading the mode from the RC20 thermostat (0x17) * received only after requested */ -void _process_RC20Set(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC20Set(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.mode = data[EMS_OFFSET_RC20Set_mode]; } @@ -1123,7 +1123,7 @@ void _process_RC20Set(uint8_t type, uint8_t * data, uint8_t length) { * type 0xA7 - for reading the mode from the RC30 thermostat (0x10) * received only after requested */ -void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC30Set(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.mode = data[EMS_OFFSET_RC30Set_mode]; } @@ -1132,14 +1132,14 @@ void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length) { * Working Mode Heating Circuit 1 (HC1) * received only after requested */ -void _process_RC35Set(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC35Set(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.mode = data[EMS_OFFSET_RC35Set_mode]; } /** * type 0xA3 - for external temp settings from the the RC* thermostats */ -void _process_RCOutdoorTempMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RCOutdoorTempMessage(uint8_t src, uint8_t * data, uint8_t length) { // add support here if you're reading external sensors } @@ -1147,7 +1147,7 @@ void _process_RCOutdoorTempMessage(uint8_t type, uint8_t * data, uint8_t length) * type 0x02 - get the firmware version and type of an EMS device * look up known devices via the product id and setup if not already set */ -void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { +void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { // ignore short messages that we can't interpret if (length < 3) { return; @@ -1265,10 +1265,53 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { return; } else { - myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", type, product_id, version); + myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", src, product_id, version); } } +/* + * SM10Monitor - type 0x97 + */ +void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { + // TODO: polish off + EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 + EMS_Other.SM10bottomTemp = _toFloat(5, data); // bottom temp from SM10 + EMS_Other.SM10pumpModulation = data[4]; // modulation solar pump +} + +/** + * UBASetPoint 0x1A + */ +void _process_SetPoints(uint8_t src, uint8_t * data, uint8_t length) { + /* + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + if (length != 0) { + uint8_t setpoint = data[0]; + uint8_t hk_power = data[1]; + uint8_t ww_power = data[2]; + myDebug(" SetPoint=%d, hk_power=%d, ww_power=%d", setpoint, hk_power, ww_power); + } + } + */ +} + +/** + * process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long + * common for all thermostats + */ +void _process_RCTime(uint8_t src, uint8_t * data, uint8_t length) { + if ((EMS_Thermostat.model_id == EMS_MODEL_EASY) || (EMS_Thermostat.model_id == EMS_MODEL_BOSCHEASY)) { + return; // not supported + } + + EMS_Thermostat.hour = data[2]; + EMS_Thermostat.minute = data[4]; + EMS_Thermostat.second = data[5]; + EMS_Thermostat.day = data[3]; + EMS_Thermostat.month = data[1]; + EMS_Thermostat.year = data[0]; +} + /* * Figure out the boiler and thermostat types */ @@ -1328,50 +1371,6 @@ void _ems_setThermostatModel(uint8_t thermostat_modelid) { EMS_Thermostat.write_supported = thermostat_type->write_supported; } -/* - * SM10Monitor - type 0x97 - */ -void _process_SM10Monitor(uint8_t type, uint8_t * data, uint8_t length) { - // TODO: polish off - EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 - EMS_Other.SM10bottomTemp = _toFloat(5, data); // bottom temp from SM10 - EMS_Other.SM10pumpModulation = data[4]; // modulation solar pump -} - - -/** - * UBASetPoint 0x1A - */ -void _process_SetPoints(uint8_t type, uint8_t * data, uint8_t length) { - /* - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - if (length != 0) { - uint8_t setpoint = data[0]; - uint8_t hk_power = data[1]; - uint8_t ww_power = data[2]; - myDebug(" SetPoint=%d, hk_power=%d, ww_power=%d", setpoint, hk_power, ww_power); - } - } - */ -} - -/** - * process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long - * common for all thermostats - */ -void _process_RCTime(uint8_t type, uint8_t * data, uint8_t length) { - if ((EMS_Thermostat.model_id == EMS_MODEL_EASY) || (EMS_Thermostat.model_id == EMS_MODEL_BOSCHEASY)) { - return; // not supported - } - - EMS_Thermostat.hour = data[2]; - EMS_Thermostat.minute = data[4]; - EMS_Thermostat.second = data[5]; - EMS_Thermostat.day = data[3]; - EMS_Thermostat.month = data[1]; - EMS_Thermostat.year = data[0]; -} - /** * Print the Tx queue - for debugging */ diff --git a/src/ems.h b/src/ems.h index 90027f4da..4a27395ff 100644 --- a/src/ems.h +++ b/src/ems.h @@ -255,7 +255,7 @@ typedef struct { } _EMS_Thermostat; // call back function signature for processing telegram types -typedef void (*EMS_processType_cb)(uint8_t type, uint8_t * data, uint8_t length); +typedef void (*EMS_processType_cb)(uint8_t src, uint8_t * data, uint8_t length); // Definition for each EMS type, including the relative callback function typedef struct { @@ -308,8 +308,8 @@ void ems_startupTelegrams(); // private functions uint8_t _crcCalculator(uint8_t * data, uint8_t len); -void _processType(_EMS_RxTelegram EMS_RxTelegram); -void _debugPrintPackage(const char * prefix, _EMS_RxTelegram EMS_RxTelegram, const char * color); +void _processType(_EMS_RxTelegram *EMS_RxTelegram); +void _debugPrintPackage(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color); void _ems_clearTxData(); int _ems_findBoilerModel(uint8_t model_id); bool _ems_setModel(uint8_t model_id); diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 6d5a88241..fb4827ea2 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -102,7 +102,7 @@ void ICACHE_FLASH_ATTR emsuart_init() { PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD); // set 9600, 8 bits, no parity check, 1 stop bit - USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD); + USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD); USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1 // flush everything left over in buffer, this clears both rx and tx FIFOs From 2094e059a17131827628163ad0539f959fa81909 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 21 Mar 2019 18:06:53 +0100 Subject: [PATCH 40/59] supress echo --- src/ems.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index 0ef2d6898..bffa3a7c2 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -815,7 +815,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { // if its an echo of ourselves from the master UBA, ignore if (src == EMS_ID_ME) { - _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE); + // _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE); return; } @@ -1273,7 +1273,6 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { * SM10Monitor - type 0x97 */ void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { - // TODO: polish off EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 EMS_Other.SM10bottomTemp = _toFloat(5, data); // bottom temp from SM10 EMS_Other.SM10pumpModulation = data[4]; // modulation solar pump From cb7e8359b68503bccb5689e470f7f4f6ace49284 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 21 Mar 2019 18:07:11 +0100 Subject: [PATCH 41/59] made delay optional. seems to have been fixed in core 2.5.0 --- src/ems-esp.ino | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index c2354ee04..0e4297145 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -31,6 +31,9 @@ DS18 ds18; #define myDebug(...) myESP.myDebug(__VA_ARGS__) #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) +// set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1. +#define EMSESP_DELAY 0 // initially set to 0 for no delay + // timers, all values are in seconds #define DEFAULT_PUBLISHWAIT 120 // every 2 minutes publish MQTT values, including Dallas sensors Ticker publishValuesTimer; @@ -1382,7 +1385,7 @@ void loop() { // the main loop myESP.loop(); - // check Dallas sensors + // check Dallas sensors, every 2 seconds if (EMSESP_Status.dallas_sensors != 0) { ds18.loop(); } @@ -1399,5 +1402,8 @@ void loop() { showerCheck(); } - delay(1); // some time to WiFi and everything else to catch up, and prevent overheating + if (EMSESP_DELAY != 0) { + delay(EMSESP_DELAY); // some time to WiFi and everything else to catch up, and prevent overheating + } + } From 190082155e5bbdacca58323c02b60960cb5aded4 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 21 Mar 2019 22:16:54 +0100 Subject: [PATCH 42/59] fixes for SM10 --- src/ems-esp.ino | 8 ++++---- src/ems.cpp | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 0e4297145..9bd74754d 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -392,9 +392,10 @@ void showInfo() { // For SM10 Solar Module if (EMS_Other.SM10) { - _renderFloatValue("SM10 collector temperature", "C", EMS_Other.SM10collectorTemp); - _renderFloatValue("SM10 bottom temperature", "C", EMS_Other.SM10bottomTemp); - _renderIntValue("SM10 pump", "%", EMS_Other.SM10pumpModulation); + myDebug("%sSolar Module stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + _renderFloatValue(" Collector temperature", "C", EMS_Other.SM10collectorTemp); + _renderFloatValue(" Bottom temperature", "C", EMS_Other.SM10bottomTemp); + _renderIntValue(" Pump modulation", "%", EMS_Other.SM10pumpModulation); } myDebug(""); // newline @@ -1405,5 +1406,4 @@ void loop() { if (EMSESP_DELAY != 0) { delay(EMSESP_DELAY); // some time to WiFi and everything else to catch up, and prevent overheating } - } diff --git a/src/ems.cpp b/src/ems.cpp index bffa3a7c2..3a5a3f272 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -1263,7 +1263,10 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { myDebug("SM10 Solar Module support enabled."); } + // fetch other values + ems_getOtherValues(); return; + } else { myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", src, product_id, version); } From 590abfa4b9f9ef962b1bcb9a2fbe3c20035b995a Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 21 Mar 2019 23:47:31 +0100 Subject: [PATCH 43/59] optimized Type detection on incoming telegrams --- src/ems-esp.ino | 6 +++--- src/ems.cpp | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 9bd74754d..5a3323e07 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -32,7 +32,7 @@ DS18 ds18; #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) // set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1. -#define EMSESP_DELAY 0 // initially set to 0 for no delay +#define EMSESP_DELAY 1 // initially set to 0 for no delay // timers, all values are in seconds #define DEFAULT_PUBLISHWAIT 120 // every 2 minutes publish MQTT values, including Dallas sensors @@ -1204,7 +1204,7 @@ void do_ledcheck() { // Thermostat scan void do_scanThermostat() { if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebug("> Scanning thermostat message type #0x%02X..", scanThermostat_count); + myDebug("> Scanning thermostat message type #0x%02X...", scanThermostat_count); ems_doReadCommand(scanThermostat_count, EMS_Thermostat.type_id); scanThermostat_count++; } @@ -1221,7 +1221,7 @@ void do_systemCheck() { // only if we have a EMS connection void do_regularUpdates() { if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebugLog("Calling scheduled data refresh from EMS devices.."); + myDebugLog("Calling scheduled data refresh from EMS devices..."); ems_getThermostatValues(); ems_getBoilerValues(); ems_getOtherValues(); diff --git a/src/ems.cpp b/src/ems.cpp index 3a5a3f272..f71be507d 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -759,25 +759,25 @@ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { } // see if we recognize the type first by scanning our known EMS types list - // trying to match the type ID - bool commonType = false; - bool typeFound = false; - bool forUs = false; - int i = 0; + bool typeFound = false; + uint8_t i = 0; while (i < _EMS_Types_max) { if (EMS_Types[i].type == type) { - typeFound = true; - commonType = (EMS_Types[i].model_id == EMS_MODEL_ALL); // is it common type for everyone? - forUs = (src == EMS_Boiler.type_id) || (src == EMS_Thermostat.type_id); // is it for us? So the src must match - break; + // is it common type for everyone? + // is it for us? So the src must match with either the boiler, thermostat or other devices + if ((EMS_Types[i].model_id == EMS_MODEL_ALL) + || ((src == EMS_Boiler.type_id) || (src == EMS_Thermostat.type_id) || (src == EMS_ID_SM10))) { + typeFound = true; + break; + } } i++; } // if it's a common type (across ems devices) or something specifically for us process it. // dest will be EMS_ID_NONE and offset 0x00 for a broadcast message - if (typeFound && (commonType || forUs)) { + if (typeFound) { if ((EMS_Types[i].processType_cb) != (void *)NULL) { // print non-verbose message if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) { @@ -785,7 +785,7 @@ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { } // call callback function to process it // as we only handle complete telegrams (not partial) check that the offset is 0 - if (offset == EMS_ID_NONE) { + if (offset == 0) { (void)EMS_Types[i].processType_cb(src, data, EMS_RxTelegram->length - 5); } } From b5ae84704819b6f1237eb6be2d2ff61e17f45206 Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 22 Mar 2019 17:21:17 +0100 Subject: [PATCH 44/59] SM10 MQTT support --- src/ems-esp.ino | 33 ++++++++++++++ src/ems.cpp | 117 ++++++++++++++++++++++++------------------------ src/ems.h | 3 +- src/my_config.h | 8 ++++ 4 files changed, 102 insertions(+), 59 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 5a3323e07..aacaf533b 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -396,6 +396,7 @@ void showInfo() { _renderFloatValue(" Collector temperature", "C", EMS_Other.SM10collectorTemp); _renderFloatValue(" Bottom temperature", "C", EMS_Other.SM10bottomTemp); _renderIntValue(" Pump modulation", "%", EMS_Other.SM10pumpModulation); + _renderBoolValue(" Pump active", EMS_Other.SM10pump); } myDebug(""); // newline @@ -486,6 +487,7 @@ void publishValues(bool force) { static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off static uint32_t previousBoilerPublishCRC = 0; // CRC check for boiler values static uint32_t previousThermostatPublishCRC = 0; // CRC check for thermostat values + static uint32_t previousOtherPublishCRC = 0; // CRC check for other values (e.g. SM10) JsonObject rootBoiler = doc.to(); @@ -595,6 +597,36 @@ void publishValues(bool force) { myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); } } + + // handle the other values separately + // For SM10 Solar Module + if (EMS_Other.SM10) { + // build new json object + doc.clear(); + JsonObject rootSM10 = doc.to(); + + rootSM10[SM10_COLLECTORTEMP] = _float_to_char(s, EMS_Other.SM10collectorTemp); + rootSM10[SM10_BOTTOMTEMP] = _float_to_char(s, EMS_Other.SM10bottomTemp); + rootSM10[SM10_PUMPMODULATION] = _int_to_char(s, EMS_Other.SM10pumpModulation); + rootSM10[SM10_PUMP] = _bool_to_char(s, EMS_Other.SM10pump); + + data[0] = '\0'; // reset data for next package + serializeJson(doc, data, sizeof(data)); + + // calculate new CRC + crc.reset(); + for (size_t i = 0; i < measureJson(doc) - 1; i++) { + crc.update(data[i]); + } + fchecksum = crc.finalize(); + if ((previousOtherPublishCRC != fchecksum) || force) { + previousOtherPublishCRC = fchecksum; + myDebugLog("Publishing SM10 data via MQTT"); + + // send values via MQTT + myESP.mqttPublish(TOPIC_SM10_DATA, data); + } + } } // sets the shower timer on/off @@ -1387,6 +1419,7 @@ void loop() { myESP.loop(); // check Dallas sensors, every 2 seconds + // these values are published to MQTT seperately via the timer publishSensorValuesTimer if (EMSESP_Status.dallas_sensors != 0) { ds18.loop(); } diff --git a/src/ems.cpp b/src/ems.cpp index f71be507d..4a2bbc025 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -238,6 +238,7 @@ void ems_init() { EMS_Other.SM10collectorTemp = EMS_VALUE_FLOAT_NOTSET; // collector temp from SM10 EMS_Other.SM10bottomTemp = EMS_VALUE_FLOAT_NOTSET; // bottom temp from SM10 EMS_Other.SM10pumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10 + EMS_Other.SM10pump = EMS_VALUE_INT_NOTSET; // pump active // calculated values EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off @@ -1143,6 +1144,51 @@ void _process_RCOutdoorTempMessage(uint8_t src, uint8_t * data, uint8_t length) // add support here if you're reading external sensors } +/* + * SM10Monitor - type 0x97 + */ +void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 + EMS_Other.SM10bottomTemp = _toFloat(5, data); // bottom temp from SM10 + EMS_Other.SM10pumpModulation = data[4]; // modulation solar pump + EMS_Other.SM10pump = bitRead(data[6], 1); // active if bit 1 is set (to 1) + + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT +} + +/** + * UBASetPoint 0x1A + */ +void _process_SetPoints(uint8_t src, uint8_t * data, uint8_t length) { + /* + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + if (length != 0) { + uint8_t setpoint = data[0]; + uint8_t hk_power = data[1]; + uint8_t ww_power = data[2]; + myDebug(" SetPoint=%d, hk_power=%d, ww_power=%d", setpoint, hk_power, ww_power); + } + } + */ +} + +/** + * process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long + * common for all thermostats + */ +void _process_RCTime(uint8_t src, uint8_t * data, uint8_t length) { + if ((EMS_Thermostat.model_id == EMS_MODEL_EASY) || (EMS_Thermostat.model_id == EMS_MODEL_BOSCHEASY)) { + return; // not supported + } + + EMS_Thermostat.hour = data[2]; + EMS_Thermostat.minute = data[4]; + EMS_Thermostat.second = data[5]; + EMS_Thermostat.day = data[3]; + EMS_Thermostat.month = data[1]; + EMS_Thermostat.year = data[0]; +} + /** * type 0x02 - get the firmware version and type of an EMS device * look up known devices via the product id and setup if not already set @@ -1272,48 +1318,6 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { } } -/* - * SM10Monitor - type 0x97 - */ -void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 - EMS_Other.SM10bottomTemp = _toFloat(5, data); // bottom temp from SM10 - EMS_Other.SM10pumpModulation = data[4]; // modulation solar pump -} - -/** - * UBASetPoint 0x1A - */ -void _process_SetPoints(uint8_t src, uint8_t * data, uint8_t length) { - /* - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - if (length != 0) { - uint8_t setpoint = data[0]; - uint8_t hk_power = data[1]; - uint8_t ww_power = data[2]; - myDebug(" SetPoint=%d, hk_power=%d, ww_power=%d", setpoint, hk_power, ww_power); - } - } - */ -} - -/** - * process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long - * common for all thermostats - */ -void _process_RCTime(uint8_t src, uint8_t * data, uint8_t length) { - if ((EMS_Thermostat.model_id == EMS_MODEL_EASY) || (EMS_Thermostat.model_id == EMS_MODEL_BOSCHEASY)) { - return; // not supported - } - - EMS_Thermostat.hour = data[2]; - EMS_Thermostat.minute = data[4]; - EMS_Thermostat.second = data[5]; - EMS_Thermostat.day = data[3]; - EMS_Thermostat.month = data[1]; - EMS_Thermostat.year = data[0]; -} - /* * Figure out the boiler and thermostat types */ @@ -1486,9 +1490,9 @@ char * ems_getThermostatDescription(char * buffer) { if (!ems_getThermostatEnabled()) { strlcpy(buffer, "", size); } else { - // find the boiler details - int i = 0; - bool found = false; + int i = 0; + bool found = false; + char tmp[6] = {0}; // scan through known ID types while (i < _Thermostat_Types_max) { @@ -1498,16 +1502,15 @@ char * ems_getThermostatDescription(char * buffer) { } i++; } + if (found) { strlcpy(buffer, Thermostat_Types[i].model_string, size); } else { - strlcpy(buffer, "Generic Type", size); + strlcpy(buffer, "TypeID: 0x", size); + strlcat(buffer, _hextoa(EMS_Thermostat.type_id, tmp), size); } - char tmp[6] = {0}; - strlcat(buffer, " [Type ID: 0x", size); - strlcat(buffer, _hextoa(EMS_Thermostat.type_id, tmp), size); - strlcat(buffer, "] Product ID:", size); + strlcat(buffer, " Product ID:", size); strlcat(buffer, itoa(EMS_Thermostat.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Thermostat.version, size); @@ -1524,9 +1527,9 @@ char * ems_getBoilerDescription(char * buffer) { if (!ems_getBoilerEnabled()) { strlcpy(buffer, "", size); } else { - // find the boiler details - int i = 0; - bool found = false; + int i = 0; + bool found = false; + char tmp[6] = {0}; // scan through known ID types while (i < _Boiler_Types_max) { @@ -1539,13 +1542,11 @@ char * ems_getBoilerDescription(char * buffer) { if (found) { strlcpy(buffer, Boiler_Types[i].model_string, size); } else { - strlcpy(buffer, "Generic Type", size); + strlcpy(buffer, "TypeID: 0x", size); + strlcat(buffer, _hextoa(EMS_Boiler.type_id, tmp), size); } - char tmp[6] = {0}; - strlcat(buffer, " [Type ID: 0x", size); - strlcat(buffer, _hextoa(EMS_Boiler.type_id, tmp), size); - strlcat(buffer, "] Product ID:", size); + strlcat(buffer, " Product ID:", size); strlcat(buffer, itoa(EMS_Boiler.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Boiler.version, size); diff --git a/src/ems.h b/src/ems.h index 4a27395ff..ddee54043 100644 --- a/src/ems.h +++ b/src/ems.h @@ -232,6 +232,7 @@ typedef struct { float SM10collectorTemp; // collector temp from SM10 float SM10bottomTemp; // bottom temp from SM10 uint8_t SM10pumpModulation; // modulation solar pump + uint8_t SM10pump; // pump active } _EMS_Other; // Thermostat data @@ -308,7 +309,7 @@ void ems_startupTelegrams(); // private functions uint8_t _crcCalculator(uint8_t * data, uint8_t len); -void _processType(_EMS_RxTelegram *EMS_RxTelegram); +void _processType(_EMS_RxTelegram * EMS_RxTelegram); void _debugPrintPackage(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color); void _ems_clearTxData(); int _ems_findBoilerModel(uint8_t model_id); diff --git a/src/my_config.h b/src/my_config.h index 9ff43a405..8f55c891f 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -39,6 +39,13 @@ #define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT #define TOPIC_BOILER_CMD_COMFORT "boiler_cmd_comfort" // for received boiler ww comfort setting via MQTT +// MQTT for SM10 Solar Module +#define TOPIC_SM10_DATA "sm10_data" // topic name +#define SM10_COLLECTORTEMP "temp" // collector temp +#define SM10_BOTTOMTEMP "bottomtemp" // bottom temp +#define SM10_PUMPMODULATION "pumpmodulation" // pump modulation +#define SM10_PUMP "pump" // pump active + // shower time #define TOPIC_SHOWERTIME "showertime" // for sending shower time results #define TOPIC_SHOWER_TIMER "shower_timer" // toggle switch for enabling the shower logic @@ -49,6 +56,7 @@ #define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT #define PAYLOAD_EXTERNAL_SENSORS "temp_%d" // for formatting the payload for each external dallas sensor + //////////////////////////////////////////////////////////////////////////////////////////////////// // THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // //////////////////////////////////////////////////////////////////////////////////////////////////// From af5a71b7c6d9e2c11d09d5d2c4baaee60f266c26 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 23 Mar 2019 11:06:46 +0100 Subject: [PATCH 45/59] added function to return raw int16 value --- src/ds18.cpp | 9 ++++++--- src/ds18.h | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ds18.cpp b/src/ds18.cpp index 563e5c30d..08bb6d7ca 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -134,7 +134,6 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { return buffer; } - /* * Read sensor values * @@ -152,7 +151,7 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { DS18B20 & DS1822: store for crc byte 8: SCRATCHPAD_CRC */ -double DS18::getValue(unsigned char index) { +int16_t DS18::getRawValue(unsigned char index) { if (index >= _count) return 0; @@ -179,8 +178,12 @@ double DS18::getValue(unsigned char index) { // 12 bit res, 750 ms } - double value = (float)raw / 16.0; + return raw; +} +// return real value as a double +double DS18::getValue(unsigned char index) { + double value = (float)getRawValue(index) / 16.0; return value; } diff --git a/src/ds18.h b/src/ds18.h index a537ecf87..e6e97b300 100644 --- a/src/ds18.h +++ b/src/ds18.h @@ -40,6 +40,7 @@ class DS18 { void loop(); char * getDeviceString(char * s, unsigned char index); double getValue(unsigned char index); + int16_t getRawValue(unsigned char index); // raw values, needs / 16 protected: bool validateID(unsigned char id); From 8ef5b4fc34bef563e47c2535ca10bfc3ffd21069 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 23 Mar 2019 11:06:58 +0100 Subject: [PATCH 46/59] bump version --- lib/MyESP/MyESP.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 23f1cc455..0c9cbfa54 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6b3" +#define MYESP_VERSION "1.1.6b4" #include #include From f773b59c7beaf7e14136e60a42188adc465c8bcc Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 23 Mar 2019 11:07:16 +0100 Subject: [PATCH 47/59] recorded changes to floats --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7c08e3b7..d651484c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.6.0 dev] 2019-03-20 +## [1.6.0 dev] 2019-03-23 ### Added @@ -13,9 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - crash command to see stack of last system crash, with .py files to track stack dump (compile with -DCRASH) - publish dallas external temp sensors to MQTT (thanks @JewelZB) - shower timer and shower alert options available via set commands -- added support for warm water modes Hot, Comfort and Intelligent (https://github.com/proddy/EMS-ESP/issues/67) +- added support for warm water modes Hot, Comfort and Intelligent [(issue 67)](https://github.com/proddy/EMS-ESP/issues/67) - added 'set publish_time' to set how often to publish MQTT -- support for SM10 Solar Module +- support for SM10 Solar Module including MQTT [(issue 77)](https://github.com/proddy/EMS-ESP/issues/77) +- 'refresh' command to force a fetch of all known data from the connected EMS devices ### Fixed @@ -30,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - upgraded MyESP library with many optimizations - test_mode renamed to silent_mode - 'set wifi' replaced with 'set wifi_ssid and set wifi_password' to allow values with spaces +- EMS values are stored in the raw format and only converted to strings when displayed or published, removing the need for parsing floats +- All temps are to one decimal place [(issue 79)](https://github.com/proddy/EMS-ESP/issues/79) ## [1.5.6] 2019-03-09 From 292cf6808acffe8818ca1a71222bef2660766115 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 23 Mar 2019 11:07:25 +0100 Subject: [PATCH 48/59] typos --- src/ems_devices.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ems_devices.h b/src/ems_devices.h index 7bfa1da23..e959895a0 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -147,16 +147,16 @@ const _Other_Type Other_Types[] = { const _Thermostat_Type Thermostat_Types[] = { {EMS_MODEL_ES73, 76, 0x10, "Sieger ES73", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC10, 79, 0x17, "RC10/Nefit Moduline 100)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC20, 77, 0x17, "RC20/Nefit Moduline 300)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC10, 79, 0x17, "RC10/Nefit Moduline 100", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC20, 77, 0x17, "RC20/Nefit Moduline 300", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC20F, 93, 0x18, "RC20F", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC30, 78, 0x10, "RC30/Nefit Moduline 400)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC30, 78, 0x10, "RC30/Nefit Moduline 400", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC35, 86, 0x10, "RC35", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_EASY, 202, 0x18, "TC100/Nefit Easy", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_BOSCHEASY, 206, 0x02, "Bosch Easy", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_RC310, 158, 0x10, "RC310", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_CW100, 255, 0x18, "Bosch CW100", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC10, 165, 0x02, "RC10/Nefit Moduline 1010)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES} + {EMS_MODEL_RC10, 165, 0x02, "RC10/Nefit Moduline 1010", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES} }; From 82e7c63251674c06c8249c2d02b8a6f2e9ff90e0 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 23 Mar 2019 11:07:35 +0100 Subject: [PATCH 49/59] version bump --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index c1d9828b5..a8377ec7c 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.6.0b4" +#define APP_VERSION "1.6.0b5" #define APP_HOSTNAME "ems-esp" From 599171202c55d1599dc6adb53cf15c0b986fd9e1 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 23 Mar 2019 11:07:59 +0100 Subject: [PATCH 50/59] remove floats --- src/ems-esp.ino | 194 ++++++++++++++++++++++---------------- src/ems.cpp | 242 ++++++++++++++++++++++-------------------------- src/ems.h | 59 ++++++------ 3 files changed, 251 insertions(+), 244 deletions(-) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index aacaf533b..9376d0687 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -101,6 +101,7 @@ command_t PROGMEM project_cmds[] = { {false, "info", "show data captured on the EMS bus"}, {false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, {false, "publish", "publish all values to MQTT"}, + {false, "refresh", "fetch values from the EMS devices"}, {false, "types", "list supported EMS telegram type IDs"}, {false, "queue", "show current Tx queue"}, {false, "autodetect", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, @@ -132,7 +133,7 @@ char * _float_to_char(char * a, float f, uint8_t precision = 2) { char * ret = a; // check for 0x8000 (sensor missing) - if (f == EMS_VALUE_FLOAT_NOTSET) { + if (f == EMS_VALUE_SHORT_NOTSET) { strlcpy(ret, "?", sizeof(ret)); } else { long whole = (long)f; @@ -158,63 +159,76 @@ char * _bool_to_char(char * s, uint8_t value) { return s; } -// convert int (single byte) to text value -char * _int_to_char(char * s, uint8_t value) { - if (value == EMS_VALUE_INT_NOTSET) { +// convert short (two bytes) to text value +// negative values are assumed stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c) +char * _short_to_char(char * s, int16_t value, uint8_t div = 10) { + // remove errors on invalid values + if (abs(value) >= EMS_VALUE_SHORT_NOTSET) { strlcpy(s, "?", sizeof(s)); + return (s); + } + + if (div != 0) { + char s2[5] = {0}; + // check for negative values + if (value < 0) { + strlcpy(s, "-", 2); + strlcat(s, itoa(abs(value) / div, s2, 10), 5); + } else { + strlcpy(s, itoa(value / div, s2, 10), 5); + } + strlcat(s, ".", sizeof(s)); + strlcat(s, itoa(abs(value) % div, s2, 10), 5); } else { itoa(value, s, 10); } return s; } -// takes a float value at prints it to debug log -void _renderFloatValue(const char * prefix, const char * postfix, float value) { - char buffer[200] = {0}; - char s[20] = {0}; - strlcpy(buffer, " ", sizeof(buffer)); - strlcat(buffer, prefix, sizeof(buffer)); - strlcat(buffer, ": ", sizeof(buffer)); - strlcat(buffer, _float_to_char(s, value), sizeof(buffer)); - - if (postfix != NULL) { - strlcat(buffer, " ", sizeof(buffer)); - strlcat(buffer, postfix, sizeof(buffer)); - } - myDebug(buffer); -} - -// takes an int (single byte) value at prints it to debug log -void _renderIntValue(const char * prefix, const char * postfix, uint8_t value) { - char buffer[200] = {0}; - char s[20] = {0}; - strlcpy(buffer, " ", sizeof(buffer)); - strlcat(buffer, prefix, sizeof(buffer)); - strlcat(buffer, ": ", sizeof(buffer)); - strlcat(buffer, _int_to_char(s, value), sizeof(buffer)); - - if (postfix != NULL) { - strlcat(buffer, " ", sizeof(buffer)); - strlcat(buffer, postfix, sizeof(buffer)); - } - myDebug(buffer); -} - -// takes an int value, converts to a fraction -void _renderIntfractionalValue(const char * prefix, const char * postfix, uint8_t value, uint8_t decimals) { - char buffer[200] = {0}; - char s[20] = {0}; - strlcpy(buffer, " ", sizeof(buffer)); - strlcat(buffer, prefix, sizeof(buffer)); - strlcat(buffer, ": ", sizeof(buffer)); - +// convert int (single byte) to text value +char * _int_to_char(char * s, uint8_t value, uint8_t div = 0) { if (value == EMS_VALUE_INT_NOTSET) { - strlcat(buffer, "?", sizeof(buffer)); + strlcpy(s, "?", sizeof(s)); } else { - strlcat(buffer, _int_to_char(s, value / (decimals * 10)), sizeof(buffer)); - strlcat(buffer, ".", sizeof(buffer)); - strlcat(buffer, _int_to_char(s, value % (decimals * 10)), sizeof(buffer)); + if (div != 0) { + char s2[5] = {0}; + strlcpy(s, itoa(value / div, s2, 10), 5); + strlcat(s, ".", sizeof(s)); + strlcat(s, itoa(value % div, s2, 10), 5); + } else { + itoa(value, s, 10); + } } + return s; +} + +// takes an int value (1 byte), converts to a fraction +void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div = 0) { + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + + strlcat(buffer, _int_to_char(s, value, div), sizeof(buffer)); + + if (postfix != NULL) { + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); + } + + myDebug(buffer); +} + +// takes a short value (2 bytes), converts to a fraction +void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t div = 10) { + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + + strlcat(buffer, _short_to_char(s, value, div), sizeof(buffer)); if (postfix != NULL) { strlcat(buffer, " ", sizeof(buffer)); @@ -308,7 +322,7 @@ void showInfo() { } if (EMS_Boiler.heatingActive != EMS_VALUE_INT_NOTSET) { - myDebug(" Central Heating: %s", EMS_Boiler.heatingActive ? "active" : "off"); + myDebug(" Central heating: %s", EMS_Boiler.heatingActive ? "active" : "off"); } } @@ -327,8 +341,8 @@ void showInfo() { _renderIntValue("Warm Water desired temperature", "C", EMS_Boiler.wWDesiredTemp); // UBAMonitorWWMessage - _renderFloatValue("Warm Water current temperature", "C", EMS_Boiler.wWCurTmp); - _renderIntfractionalValue("Warm Water current tap water flow", "l/min", EMS_Boiler.wWCurFlow, 1); + _renderShortValue("Warm Water current temperature", "C", EMS_Boiler.wWCurTmp); + _renderIntValue("Warm Water current tap water flow", "l/min", EMS_Boiler.wWCurFlow, 10); _renderLongValue("Warm Water # starts", "times", EMS_Boiler.wWStarts); if (EMS_Boiler.wWWorkM != EMS_VALUE_LONG_NOTSET) { myDebug(" Warm Water active time: %d days %d hours %d minutes", @@ -340,8 +354,8 @@ void showInfo() { // UBAMonitorFast _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); - _renderFloatValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); - _renderFloatValue("Return temperature", "C", EMS_Boiler.retTemp); + _renderShortValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); + _renderShortValue("Return temperature", "C", EMS_Boiler.retTemp); _renderBoolValue("Gas", EMS_Boiler.burnGas); _renderBoolValue("Boiler pump", EMS_Boiler.heatPmp); _renderBoolValue("Fan", EMS_Boiler.fanWork); @@ -349,8 +363,8 @@ void showInfo() { _renderBoolValue("Circulation pump", EMS_Boiler.wWCirc); _renderIntValue("Burner selected max power", "%", EMS_Boiler.selBurnPow); _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); - _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); - _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); + _renderShortValue("Flame current", "uA", EMS_Boiler.flameCurr); + _renderIntValue("System pressure", "bar", EMS_Boiler.sysPress, 10); if (EMS_Boiler.serviceCode == EMS_VALUE_SHORT_NOTSET) { myDebug(" System service code: %s", EMS_Boiler.serviceCodeChar); } else { @@ -359,14 +373,14 @@ void showInfo() { // UBAParametersMessage _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); - _renderIntValue("Boiler circuit pump modulation max. power", "%", EMS_Boiler.pump_mod_max); - _renderIntValue("Boiler circuit pump modulation min. power", "%", EMS_Boiler.pump_mod_min); + _renderIntValue("Boiler circuit pump modulation max power", "%", EMS_Boiler.pump_mod_max); + _renderIntValue("Boiler circuit pump modulation min power", "%", EMS_Boiler.pump_mod_min); // UBAMonitorSlow - if (EMS_Boiler.extTemp != EMS_VALUE_FLOAT_NOTSET) { - _renderFloatValue("Outside temperature", "C", EMS_Boiler.extTemp); + if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) { + _renderShortValue("Outside temperature", "C", EMS_Boiler.extTemp); } - _renderFloatValue("Boiler temperature", "C", EMS_Boiler.boilTemp); + _renderShortValue("Boiler temperature", "C", EMS_Boiler.boilTemp); _renderIntValue("Pump modulation", "%", EMS_Boiler.pumpMod); _renderLongValue("Burner # starts", "times", EMS_Boiler.burnStarts); if (EMS_Boiler.burnWorkMin != EMS_VALUE_LONG_NOTSET) { @@ -393,8 +407,8 @@ void showInfo() { // For SM10 Solar Module if (EMS_Other.SM10) { myDebug("%sSolar Module stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); - _renderFloatValue(" Collector temperature", "C", EMS_Other.SM10collectorTemp); - _renderFloatValue(" Bottom temperature", "C", EMS_Other.SM10bottomTemp); + _renderShortValue(" Collector temperature", "C", EMS_Other.SM10collectorTemp); + _renderShortValue(" Bottom temperature", "C", EMS_Other.SM10bottomTemp); _renderIntValue(" Pump modulation", "%", EMS_Other.SM10pumpModulation); _renderBoolValue(" Pump active", EMS_Other.SM10pump); } @@ -405,9 +419,15 @@ void showInfo() { if (ems_getThermostatEnabled()) { myDebug("%sThermostat stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug(" Thermostat type: %s", ems_getThermostatDescription(buffer_type)); - _renderFloatValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp); - _renderFloatValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp); - if ((ems_getThermostatModel() != EMS_MODEL_EASY) && (ems_getThermostatModel() != EMS_MODEL_BOSCHEASY)) { + if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_BOSCHEASY)) { + // for easy temps are * 100 + // also we don't have the time or mode + _renderShortValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 100); + _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 100); + } else { + _renderShortValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 2); + _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 10); + myDebug(" Thermostat time is %02d:%02d:%02d %d/%d/%d", EMS_Thermostat.hour, EMS_Thermostat.minute, @@ -436,7 +456,7 @@ void showInfo() { myDebug("%sExternal temperature sensors:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { snprintf(s, sizeof(s), "Sensor #%d %s", i + 1, ds18.getDeviceString(buffer, i)); - _renderFloatValue(s, "C", ds18.getValue(i)); + _renderShortValue(s, "C", ds18.getRawValue(i), 16); // divide by 16 } myDebug(""); // newline } @@ -492,8 +512,8 @@ void publishValues(bool force) { JsonObject rootBoiler = doc.to(); rootBoiler["wWSelTemp"] = _int_to_char(s, EMS_Boiler.wWSelTemp); - rootBoiler["selFlowTemp"] = _float_to_char(s, EMS_Boiler.selFlowTemp); - rootBoiler["outdoorTemp"] = _float_to_char(s, EMS_Boiler.extTemp); + rootBoiler["selFlowTemp"] = _int_to_char(s, EMS_Boiler.selFlowTemp); + rootBoiler["outdoorTemp"] = _short_to_char(s, EMS_Boiler.extTemp); rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { @@ -504,12 +524,12 @@ void publishValues(bool force) { rootBoiler["wWComfort"] = "Intelligent"; } - rootBoiler["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); + rootBoiler["wWCurTmp"] = _short_to_char(s, EMS_Boiler.wWCurTmp); snprintf(s, sizeof(s), "%i.%i", EMS_Boiler.wWCurFlow / 10, EMS_Boiler.wWCurFlow % 10); rootBoiler["wWCurFlow"] = s; rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); - rootBoiler["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp); - rootBoiler["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp); + rootBoiler["curFlowTemp"] = _short_to_char(s, EMS_Boiler.curFlowTemp); + rootBoiler["retTemp"] = _short_to_char(s, EMS_Boiler.retTemp); rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); @@ -517,8 +537,8 @@ void publishValues(bool force) { rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); rootBoiler["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); rootBoiler["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); - rootBoiler["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); - rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); + rootBoiler["sysPress"] = _int_to_char(s, EMS_Boiler.sysPress, 10); + rootBoiler["boilTemp"] = _short_to_char(s, EMS_Boiler.boilTemp); rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; @@ -551,15 +571,20 @@ void publishValues(bool force) { // handle the thermostat values separately if (ems_getThermostatEnabled()) { // only send thermostat values if we actually have them - if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) + if ((EMS_Thermostat.curr_roomTemp == 0) || (EMS_Thermostat.setpoint_roomTemp == 0)) return; // build new json object doc.clear(); JsonObject rootThermostat = doc.to(); - rootThermostat[THERMOSTAT_CURRTEMP] = _float_to_char(s, EMS_Thermostat.curr_roomTemp); - rootThermostat[THERMOSTAT_SELTEMP] = _float_to_char(s, EMS_Thermostat.setpoint_roomTemp); + if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_BOSCHEASY)) { + rootThermostat[THERMOSTAT_CURRTEMP] = _short_to_char(s, EMS_Thermostat.curr_roomTemp, 100); + rootThermostat[THERMOSTAT_SELTEMP] = _short_to_char(s, EMS_Thermostat.setpoint_roomTemp, 100); + } else { + rootThermostat[THERMOSTAT_CURRTEMP] = _short_to_char(s, EMS_Thermostat.curr_roomTemp, 10); + rootThermostat[THERMOSTAT_SELTEMP] = _short_to_char(s, EMS_Thermostat.setpoint_roomTemp, 2); + } // RC20 has different mode settings if (ems_getThermostatModel() == EMS_MODEL_RC20) { @@ -605,8 +630,8 @@ void publishValues(bool force) { doc.clear(); JsonObject rootSM10 = doc.to(); - rootSM10[SM10_COLLECTORTEMP] = _float_to_char(s, EMS_Other.SM10collectorTemp); - rootSM10[SM10_BOTTOMTEMP] = _float_to_char(s, EMS_Other.SM10bottomTemp); + rootSM10[SM10_COLLECTORTEMP] = _short_to_char(s, EMS_Other.SM10collectorTemp); + rootSM10[SM10_BOTTOMTEMP] = _short_to_char(s, EMS_Other.SM10bottomTemp); rootSM10[SM10_PUMPMODULATION] = _int_to_char(s, EMS_Other.SM10pumpModulation); rootSM10[SM10_PUMP] = _bool_to_char(s, EMS_Other.SM10pump); @@ -925,6 +950,12 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { ok = true; } + if (strcmp(first_cmd, "refresh") == 0) { + myDebug("Fetching data from EMS devices..."); + do_regularUpdates(); + ok = true; + } + if (strcmp(first_cmd, "types") == 0) { ems_printAllTypes(); ok = true; @@ -1118,10 +1149,9 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { // boiler wwtemp changes if (strcmp(topic, TOPIC_BOILER_CMD_WWTEMP) == 0) { - float f = strtof((char *)message, 0); - char s[10] = {0}; - myDebug("MQTT topic: boiler warm water temperature value %s", _float_to_char(s, f)); - ems_setWarmWaterTemp(f); + uint8_t t = atoi((char *)message); + myDebug("MQTT topic: boiler warm water temperature value %d", t); + ems_setWarmWaterTemp(t); publishValues(true); // publish back immediately } diff --git a/src/ems.cpp b/src/ems.cpp index 4a2bbc025..31cf80478 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -14,7 +14,12 @@ #include #include // std::list -// myESP +#define _toByte(i) (data[i]) +#define _toShort(i) ((data[i] << 8) + data[i + 1]) +#define _toLong(i) ((data[i] << 16) + (data[i + 1] << 8) + (data[i + 2])) +#define _bitRead(i, bit) (((data[i]) >> (bit)) & 0x01) + +// myESP for logging to telnet and serial #define myDebug(...) myESP.myDebug(__VA_ARGS__) _EMS_Sys_Status EMS_Sys_Status; // EMS Status @@ -128,9 +133,9 @@ uint8_t _Other_Types_max = ArraySize(Other_Types); // number of other uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types // these structs contain the data we store from the Boiler and Thermostat -_EMS_Boiler EMS_Boiler; -_EMS_Thermostat EMS_Thermostat; -_EMS_Other EMS_Other; +_EMS_Boiler EMS_Boiler; // for boiler +_EMS_Thermostat EMS_Thermostat; // for thermostat +_EMS_Other EMS_Other; // for other known EMS devices // CRC lookup table with poly 12 for faster checking const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, @@ -172,8 +177,8 @@ void ems_init() { EMS_Sys_Status.txRetryCount = 0; // thermostat - EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_FLOAT_NOTSET; - EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET; + EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_SHORT_NOTSET; + EMS_Thermostat.curr_roomTemp = EMS_VALUE_SHORT_NOTSET; EMS_Thermostat.hour = 0; EMS_Thermostat.minute = 0; EMS_Thermostat.second = 0; @@ -196,8 +201,8 @@ void ems_init() { // UBAMonitorFast EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow temperature - EMS_Boiler.curFlowTemp = EMS_VALUE_FLOAT_NOTSET; // Current flow temperature - EMS_Boiler.retTemp = EMS_VALUE_FLOAT_NOTSET; // Return temperature + EMS_Boiler.curFlowTemp = EMS_VALUE_SHORT_NOTSET; // Current flow temperature + EMS_Boiler.retTemp = EMS_VALUE_SHORT_NOTSET; // Return temperature EMS_Boiler.burnGas = EMS_VALUE_INT_NOTSET; // Gas on/off EMS_Boiler.fanWork = EMS_VALUE_INT_NOTSET; // Fan on/off EMS_Boiler.ignWork = EMS_VALUE_INT_NOTSET; // Ignition on/off @@ -206,21 +211,21 @@ void ems_init() { EMS_Boiler.wWCirc = EMS_VALUE_INT_NOTSET; // Circulation on/off EMS_Boiler.selBurnPow = EMS_VALUE_INT_NOTSET; // Burner max power EMS_Boiler.curBurnPow = EMS_VALUE_INT_NOTSET; // Burner current power - EMS_Boiler.flameCurr = EMS_VALUE_FLOAT_NOTSET; // Flame current in micro amps - EMS_Boiler.sysPress = EMS_VALUE_FLOAT_NOTSET; // System pressure + EMS_Boiler.flameCurr = EMS_VALUE_SHORT_NOTSET; // Flame current in micro amps + EMS_Boiler.sysPress = EMS_VALUE_INT_NOTSET; // System pressure strlcpy(EMS_Boiler.serviceCodeChar, "??", sizeof(EMS_Boiler.serviceCodeChar)); EMS_Boiler.serviceCode = EMS_VALUE_SHORT_NOTSET; // UBAMonitorSlow - EMS_Boiler.extTemp = EMS_VALUE_FLOAT_NOTSET; // Outside temperature - EMS_Boiler.boilTemp = EMS_VALUE_FLOAT_NOTSET; // Boiler temperature + EMS_Boiler.extTemp = EMS_VALUE_SHORT_NOTSET; // Outside temperature + EMS_Boiler.boilTemp = EMS_VALUE_SHORT_NOTSET; // Boiler temperature EMS_Boiler.pumpMod = EMS_VALUE_INT_NOTSET; // Pump modulation EMS_Boiler.burnStarts = EMS_VALUE_LONG_NOTSET; // # burner restarts EMS_Boiler.burnWorkMin = EMS_VALUE_LONG_NOTSET; // Total burner operating time EMS_Boiler.heatWorkMin = EMS_VALUE_LONG_NOTSET; // Total heat operating time // UBAMonitorWWMessage - EMS_Boiler.wWCurTmp = EMS_VALUE_FLOAT_NOTSET; // Warm Water current temperature: + EMS_Boiler.wWCurTmp = EMS_VALUE_SHORT_NOTSET; // Warm Water current temperature: EMS_Boiler.wWStarts = EMS_VALUE_LONG_NOTSET; // Warm Water # starts EMS_Boiler.wWWorkM = EMS_VALUE_LONG_NOTSET; // Warm Water # minutes EMS_Boiler.wWOneTime = EMS_VALUE_INT_NOTSET; // Warm Water one time function on/off @@ -235,8 +240,8 @@ void ems_init() { EMS_Boiler.pump_mod_min = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation min. power // Other EMS devices values - EMS_Other.SM10collectorTemp = EMS_VALUE_FLOAT_NOTSET; // collector temp from SM10 - EMS_Other.SM10bottomTemp = EMS_VALUE_FLOAT_NOTSET; // bottom temp from SM10 + EMS_Other.SM10collectorTemp = EMS_VALUE_SHORT_NOTSET; // collector temp from SM10 + EMS_Other.SM10bottomTemp = EMS_VALUE_SHORT_NOTSET; // bottom temp from SM10 EMS_Other.SM10pumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10 EMS_Other.SM10pump = EMS_VALUE_INT_NOTSET; // pump active @@ -346,52 +351,7 @@ uint8_t _crcCalculator(uint8_t * data, uint8_t len) { return crc; } -/** - * function to turn a telegram int (2 bytes) to a float. The source is *10 - * negative values are stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c) - */ -float _toFloat(uint8_t i, uint8_t * data) { - // if the MSB is set, it's a negative number or an error - if ((data[i] & 0x80) == 0x80) { - // check if its an invalid number - // 0x8000 is used when sensor is missing - if ((data[i] >= 0x80) && (data[i + 1] == 0)) { - return (float)EMS_VALUE_FLOAT_NOTSET; // return -1 to indicate that is unknown - } - // its definitely a negative number - // assume its 1-compliment, otherwise we need add 1 to the total for 2-compliment - int16_t x = (data[i] << 8) + data[i + 1]; - return ((float)(x)) / 10; - } else { - // ...a positive number - return ((float)(((data[i] << 8) + data[i + 1]))) / 10; - } -} - -// function to turn a telegram long (3 bytes) to a long int -uint32_t _toLong(uint8_t i, uint8_t * data) { - return (((data[i]) << 16) + ((data[i + 1]) << 8) + (data[i + 2])); -} - -/** - * Find the pointer to the EMS_Types array for a given type ID - */ -int _ems_findType(uint8_t type) { - uint8_t i = 0; - bool typeFound = false; - // scan through known ID types - while (i < _EMS_Types_max) { - if (EMS_Types[i].type == type) { - typeFound = true; // we have a match - break; - } - i++; - } - - return (typeFound ? i : -1); -} - -// like itoa but for hex, and quick +// like itoa but for hex, and quicker char * _hextoa(uint8_t value, char * buffer) { char * p = buffer; byte nib1 = (value >> 4) & 0x0F; @@ -421,6 +381,25 @@ char * _smallitoa3(uint16_t value, char * buffer) { return buffer; } +/** + * Find the pointer to the EMS_Types array for a given type ID + * or -1 if not found + */ +int _ems_findType(uint8_t type) { + uint8_t i = 0; + bool typeFound = false; + // scan through known ID types + while (i < _EMS_Types_max) { + if (EMS_Types[i].type == type) { + typeFound = true; // we have a match + break; + } + i++; + } + + return (typeFound ? i : -1); +} + /** * debug print a telegram to telnet/serial including the CRC * len is length in bytes including the CRC @@ -946,11 +925,11 @@ void _checkActive() { * received only after requested (not broadcasted) */ void _process_UBAParameterWW(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Boiler.wWActivated = (data[1] == 0xFF); // 0xFF means on - EMS_Boiler.wWSelTemp = data[2]; - EMS_Boiler.wWCircPump = (data[6] == 0xFF); // 0xFF means on - EMS_Boiler.wWDesiredTemp = data[8]; - EMS_Boiler.wWComfort = data[EMS_OFFSET_UBAParameterWW_wwComfort]; + EMS_Boiler.wWActivated = (_toByte(1) == 0xFF); // 0xFF means on + EMS_Boiler.wWSelTemp = _toByte(2); + EMS_Boiler.wWCircPump = (_toByte(6) == 0xFF); // 0xFF means on + EMS_Boiler.wWDesiredTemp = _toByte(8); + EMS_Boiler.wWComfort = _toByte(EMS_OFFSET_UBAParameterWW_wwComfort); EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish } @@ -960,7 +939,7 @@ void _process_UBAParameterWW(uint8_t src, uint8_t * data, uint8_t length) { * received only after requested (not broadcasted) */ void _process_UBATotalUptimeMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Boiler.UBAuptime = _toLong(0, data); + EMS_Boiler.UBAuptime = _toLong(0); EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish } @@ -968,9 +947,9 @@ void _process_UBATotalUptimeMessage(uint8_t src, uint8_t * data, uint8_t length) * UBAParametersMessage - type 0x16 */ void _process_UBAParametersMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Boiler.heating_temp = data[1]; - EMS_Boiler.pump_mod_max = data[9]; - EMS_Boiler.pump_mod_min = data[10]; + EMS_Boiler.heating_temp = _toByte(1); + EMS_Boiler.pump_mod_max = _toByte(9); + EMS_Boiler.pump_mod_min = _toByte(10); } /** @@ -978,11 +957,11 @@ void _process_UBAParametersMessage(uint8_t src, uint8_t * data, uint8_t length) * received every 10 seconds */ void _process_UBAMonitorWWMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Boiler.wWCurTmp = _toFloat(1, data); - EMS_Boiler.wWStarts = _toLong(13, data); - EMS_Boiler.wWWorkM = _toLong(10, data); - EMS_Boiler.wWOneTime = bitRead(data[5], 1); - EMS_Boiler.wWCurFlow = data[9]; + EMS_Boiler.wWCurTmp = _toShort(1); + EMS_Boiler.wWStarts = _toLong(13); + EMS_Boiler.wWWorkM = _toLong(10); + EMS_Boiler.wWOneTime = _bitRead(5, 1); + EMS_Boiler.wWCurFlow = _toByte(9); } /** @@ -990,36 +969,32 @@ void _process_UBAMonitorWWMessage(uint8_t src, uint8_t * data, uint8_t length) { * received every 10 seconds */ void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Boiler.selFlowTemp = data[0]; - EMS_Boiler.curFlowTemp = _toFloat(1, data); - EMS_Boiler.retTemp = _toFloat(13, data); + EMS_Boiler.selFlowTemp = _toByte(0); + EMS_Boiler.curFlowTemp = _toShort(1); + EMS_Boiler.retTemp = _toShort(13); - uint8_t v = data[7]; - EMS_Boiler.burnGas = bitRead(v, 0); - EMS_Boiler.fanWork = bitRead(v, 2); - EMS_Boiler.ignWork = bitRead(v, 3); - EMS_Boiler.heatPmp = bitRead(v, 5); - EMS_Boiler.wWHeat = bitRead(v, 6); - EMS_Boiler.wWCirc = bitRead(v, 7); + EMS_Boiler.burnGas = _bitRead(7, 0); + EMS_Boiler.fanWork = _bitRead(7, 2); + EMS_Boiler.ignWork = _bitRead(7, 3); + EMS_Boiler.heatPmp = _bitRead(7, 5); + EMS_Boiler.wWHeat = _bitRead(7, 6); + EMS_Boiler.wWCirc = _bitRead(7, 7); - EMS_Boiler.curBurnPow = data[4]; - EMS_Boiler.selBurnPow = data[3]; // burn power max setting + EMS_Boiler.curBurnPow = _toByte(4); + EMS_Boiler.selBurnPow = _toByte(3); // burn power max setting - EMS_Boiler.flameCurr = _toFloat(15, data); + EMS_Boiler.flameCurr = _toShort(15); // read the service code / installation status as appears on the display - EMS_Boiler.serviceCodeChar[0] = char(data[18]); // ascii character 1 - EMS_Boiler.serviceCodeChar[1] = char(data[19]); // ascii character 2 + EMS_Boiler.serviceCodeChar[0] = char(_toByte(18)); // ascii character 1 + EMS_Boiler.serviceCodeChar[1] = char(_toByte(19)); // ascii character 2 EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string // read error code - EMS_Boiler.serviceCode = (data[20] << 8) + data[21]; + EMS_Boiler.serviceCode = _toShort(20); - if (data[17] == 0xFF) { // missing value for system pressure - EMS_Boiler.sysPress = 0; - } else { - EMS_Boiler.sysPress = (((float)data[17]) / (float)10); - } + // system pressure. FF means missing + EMS_Boiler.sysPress = _toByte(17); // this is *10 // at this point do a quick check to see if the hot water or heating is active _checkActive(); @@ -1030,23 +1005,23 @@ void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length) { * received every 60 seconds */ void _process_UBAMonitorSlow(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Boiler.extTemp = _toFloat(0, data); // 0x8000 if not available - EMS_Boiler.boilTemp = _toFloat(2, data); // 0x8000 if not available - EMS_Boiler.pumpMod = data[9]; - EMS_Boiler.burnStarts = _toLong(10, data); - EMS_Boiler.burnWorkMin = _toLong(13, data); - EMS_Boiler.heatWorkMin = _toLong(19, data); + EMS_Boiler.extTemp = _toShort(0); // 0x8000 if not available + EMS_Boiler.boilTemp = _toShort(2); // 0x8000 if not available + EMS_Boiler.pumpMod = _toByte(9); + EMS_Boiler.burnStarts = _toLong(10); + EMS_Boiler.burnWorkMin = _toLong(13); + EMS_Boiler.heatWorkMin = _toLong(19); } - /** * type 0xB1 - data from the RC10 thermostat (0x17) * For reading the temp values only * received every 60 seconds + * e.g. 17 0B 91 00 80 1E 00 CB 27 00 00 00 00 05 01 00 CB 00 (CRC=47), #data=14 */ void _process_RC10StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_setpoint]) / (float)2; - EMS_Thermostat.curr_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_curr]) / (float)10; + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC10StatusMessage_setpoint); // is * 2 + EMS_Thermostat.curr_roomTemp = _toByte(EMS_TYPE_RC10StatusMessage_curr); // is * 10 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1057,8 +1032,8 @@ void _process_RC10StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { * received every 60 seconds */ void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC20StatusMessage_setpoint]) / (float)2; - EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC20StatusMessage_curr, data); + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC20StatusMessage_setpoint); // is * 2 + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC20StatusMessage_curr); // is * 10 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1069,8 +1044,8 @@ void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { * received every 60 seconds */ void _process_RC30StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC30StatusMessage_setpoint]) / (float)2; - EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC30StatusMessage_curr, data); + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC30StatusMessage_setpoint); // is * 2 + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC30StatusMessage_curr); // note, its 2 bytes here EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1081,13 +1056,13 @@ void _process_RC30StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { * received every 60 seconds */ void _process_RC35StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC35StatusMessage_setpoint]) / (float)2; + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC35StatusMessage_setpoint); // is * 2 // check if temp sensor is unavailable if ((data[0] == 0x7D) && (data[1] = 0x00)) { - EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET; + EMS_Thermostat.curr_roomTemp = EMS_VALUE_SHORT_NOTSET; } else { - EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC35StatusMessage_curr, data); + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC35StatusMessage_curr); } EMS_Thermostat.day_mode = bitRead(data[EMS_OFFSET_RC35Get_mode_day], 1); //get day mode flag EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT @@ -1095,11 +1070,11 @@ void _process_RC35StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { /** * type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long - * The Easy has a digital precision of its floats to 2 decimal places, so values is divided by 100 + * The Easy has a digital precision of its floats to 2 decimal places, so values must be divided by 100 */ void _process_EasyStatusMessage(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.curr_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_curr] << 8) + data[9]))) / 100; - EMS_Thermostat.setpoint_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_setpoint] << 8) + data[11]))) / 100; + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_EasyStatusMessage_curr); // is *100 + EMS_Thermostat.setpoint_roomTemp = _toShort(EMS_TYPE_EasyStatusMessage_setpoint); // is *100 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1117,7 +1092,7 @@ void _process_RC10Set(uint8_t src, uint8_t * data, uint8_t length) { * received only after requested */ void _process_RC20Set(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.mode = data[EMS_OFFSET_RC20Set_mode]; + EMS_Thermostat.mode = _toByte(EMS_OFFSET_RC20Set_mode); } /** @@ -1125,7 +1100,7 @@ void _process_RC20Set(uint8_t src, uint8_t * data, uint8_t length) { * received only after requested */ void _process_RC30Set(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.mode = data[EMS_OFFSET_RC30Set_mode]; + EMS_Thermostat.mode = _toByte(EMS_OFFSET_RC30Set_mode); } /** @@ -1134,7 +1109,7 @@ void _process_RC30Set(uint8_t src, uint8_t * data, uint8_t length) { * received only after requested */ void _process_RC35Set(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Thermostat.mode = data[EMS_OFFSET_RC35Set_mode]; + EMS_Thermostat.mode = _toByte(EMS_OFFSET_RC35Set_mode); } /** @@ -1148,10 +1123,10 @@ void _process_RCOutdoorTempMessage(uint8_t src, uint8_t * data, uint8_t length) * SM10Monitor - type 0x97 */ void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { - EMS_Other.SM10collectorTemp = _toFloat(2, data); // collector temp from SM10 - EMS_Other.SM10bottomTemp = _toFloat(5, data); // bottom temp from SM10 - EMS_Other.SM10pumpModulation = data[4]; // modulation solar pump - EMS_Other.SM10pump = bitRead(data[6], 1); // active if bit 1 is set (to 1) + EMS_Other.SM10collectorTemp = _toShort(2); // collector temp from SM10, is *10 + EMS_Other.SM10bottomTemp = _toShort(5); // bottom temp from SM10, is *10 + EMS_Other.SM10pumpModulation = _toByte(4); // modulation solar pump + EMS_Other.SM10pump = _bitRead(5, 1); // active if bit 1 is set (to 1) EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1181,12 +1156,12 @@ void _process_RCTime(uint8_t src, uint8_t * data, uint8_t length) { return; // not supported } - EMS_Thermostat.hour = data[2]; - EMS_Thermostat.minute = data[4]; - EMS_Thermostat.second = data[5]; - EMS_Thermostat.day = data[3]; - EMS_Thermostat.month = data[1]; - EMS_Thermostat.year = data[0]; + EMS_Thermostat.hour = _toByte(2); + EMS_Thermostat.minute = _toByte(4); + EMS_Thermostat.second = _toByte(5); + EMS_Thermostat.day = _toByte(3); + EMS_Thermostat.month = _toByte(1); + EMS_Thermostat.year = _toByte(0); } /** @@ -1199,9 +1174,9 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { return; } - uint8_t product_id = data[0]; + uint8_t product_id = _toByte(0); char version[10] = {0}; - snprintf(version, sizeof(version), "%02d.%02d", data[1], data[2]); + snprintf(version, sizeof(version), "%02d.%02d", _toByte(1), _toByte(2)); // see if its a known boiler int i = 0; @@ -1510,10 +1485,11 @@ char * ems_getThermostatDescription(char * buffer) { strlcat(buffer, _hextoa(EMS_Thermostat.type_id, tmp), size); } - strlcat(buffer, " Product ID:", size); + strlcat(buffer, " (Product ID:", size); strlcat(buffer, itoa(EMS_Thermostat.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Thermostat.version, size); + strlcat(buffer, ")", size); } return buffer; @@ -1546,10 +1522,11 @@ char * ems_getBoilerDescription(char * buffer) { strlcat(buffer, _hextoa(EMS_Boiler.type_id, tmp), size); } - strlcat(buffer, " Product ID:", size); + strlcat(buffer, " (Product ID:", size); strlcat(buffer, itoa(EMS_Boiler.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Boiler.version, size); + strlcat(buffer, ")", size); } return buffer; @@ -1635,6 +1612,7 @@ void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh) { // if we're preventing all outbound traffic, quit if (EMS_Sys_Status.emsTxDisabled) { + myDebug("in Silent Mode. All Tx is disabled."); return; } diff --git a/src/ems.h b/src/ems.h index ddee54043..8211d76e8 100644 --- a/src/ems.h +++ b/src/ems.h @@ -27,9 +27,8 @@ #define EMS_VALUE_INT_ON 1 // boolean true #define EMS_VALUE_INT_OFF 0 // boolean false #define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit ints +#define EMS_VALUE_SHORT_NOTSET 0x8000 // for 2-byte shorts #define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs -#define EMS_VALUE_SHORT_NOTSET 0xFFFF // for 2-byte shorts -#define EMS_VALUE_FLOAT_NOTSET -255 // float #define EMS_THERMOSTAT_READ_YES true #define EMS_THERMOSTAT_READ_NO false @@ -175,8 +174,8 @@ typedef struct { // UBAParameterWW // UBAMonitorFast uint8_t selFlowTemp; // Selected flow temperature - float curFlowTemp; // Current flow temperature - float retTemp; // Return temperature + int16_t curFlowTemp; // Current flow temperature + int16_t retTemp; // Return temperature uint8_t burnGas; // Gas on/off uint8_t fanWork; // Fan on/off uint8_t ignWork; // Ignition on/off @@ -185,21 +184,21 @@ typedef struct { // UBAParameterWW uint8_t wWCirc; // Circulation on/off uint8_t selBurnPow; // Burner max power uint8_t curBurnPow; // Burner current power - float flameCurr; // Flame current in micro amps - float sysPress; // System pressure + uint16_t flameCurr; // Flame current in micro amps + uint8_t sysPress; // System pressure char serviceCodeChar[3]; // 2 character status/service code uint16_t serviceCode; // error/service code // UBAMonitorSlow - float extTemp; // Outside temperature - float boilTemp; // Boiler temperature + int16_t extTemp; // Outside temperature + int16_t boilTemp; // Boiler temperature uint8_t pumpMod; // Pump modulation uint32_t burnStarts; // # burner starts uint32_t burnWorkMin; // Total burner operating time uint32_t heatWorkMin; // Total heat operating time // UBAMonitorWWMessage - float wWCurTmp; // Warm Water current temperature: + int16_t wWCurTmp; // Warm Water current temperature: uint32_t wWStarts; // Warm Water # starts uint32_t wWWorkM; // Warm Water # minutes uint8_t wWOneTime; // Warm Water one time function on/off @@ -228,31 +227,31 @@ typedef struct { // UBAParameterWW */ typedef struct { // SM10 Solar Module - SM10Monitor - bool SM10; // set true if there is a SM10 available - float SM10collectorTemp; // collector temp from SM10 - float SM10bottomTemp; // bottom temp from SM10 - uint8_t SM10pumpModulation; // modulation solar pump - uint8_t SM10pump; // pump active + bool SM10; // set true if there is a SM10 available + int16_t SM10collectorTemp; // collector temp from SM10 + int16_t SM10bottomTemp; // bottom temp from SM10 + uint8_t SM10pumpModulation; // modulation solar pump + uint8_t SM10pump; // pump active } _EMS_Other; // Thermostat data typedef struct { - uint8_t type_id; // the type ID of the thermostat - uint8_t model_id; // which Thermostat type - uint8_t product_id; - bool read_supported; - bool write_supported; - char version[10]; - float setpoint_roomTemp; // current set temp - float curr_roomTemp; // current room temp - uint8_t mode; // 0=low, 1=manual, 2=auto - bool day_mode; // 0=night, 1=day - uint8_t hour; - uint8_t minute; - uint8_t second; - uint8_t day; - uint8_t month; - uint8_t year; + uint8_t type_id; // the type ID of the thermostat + uint8_t model_id; // which Thermostat type + uint8_t product_id; + bool read_supported; + bool write_supported; + char version[10]; + int16_t setpoint_roomTemp; // current set temp + int16_t curr_roomTemp; // current room temp + uint8_t mode; // 0=low, 1=manual, 2=auto + bool day_mode; // 0=night, 1=day + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t day; + uint8_t month; + uint8_t year; } _EMS_Thermostat; // call back function signature for processing telegram types From c7c07eb1c4e67e46374374f664fef2f38a298db6 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 11:54:49 +0100 Subject: [PATCH 51/59] improvements to rendering floats --- README.md | 3 +- lib/MyESP/MyESP.h | 2 +- src/ds18.cpp | 1 + src/{ems-esp.ino => ems-esp.cpp} | 278 +++++++++++++++++-------------- src/ems.cpp | 41 ++--- src/version.h | 2 +- 6 files changed, 177 insertions(+), 150 deletions(-) rename src/{ems-esp.ino => ems-esp.cpp} (95%) diff --git a/README.md b/README.md index d62cd38f9..f0565c2cd 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ There are 3 parts to this project, first the design of the circuit, secondly the - [Home Assistant Configuration](#home-assistant-configuration) - [Building The Firmware](#building-the-firmware) - [Using PlatformIO Standalone](#using-platformio-standalone) - - [Building Using Arduino IDE](#building-using-arduino-ide) - [Using the Pre-built Firmware](#using-the-pre-built-firmware) - [Troubleshooting](#troubleshooting) - [Known Issues](#known-issues) @@ -83,7 +82,7 @@ The code and circuit has been tested with a few ESP8266 development boards such 13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5mm service jack if you have an bbqkees board. 14. Reboot the ESP, either by the reset switch or pulling the power. 15. The ESP will first perform an autodetect to try and discover the EMS devices attached. If your boiler and thermostat are recognized it will set these types and store them for ever and ever. You can trace the output by telnet'ing to the board `telnet ems-esp.local`. Also use `info` to check the status. -16. If your boiler/thermostat is not discovered create a GitHub issue stating the type and product ID. These will be added to the file `ems_devices.h` in a future release. +16. If your boiler/thermostat is not discovered create a GitHub issue stating the type and Product ID. These will be added to the file `ems_devices.h` in a future release. 17. If all is well and there is traffic on the EMS bus the onboard LED will stop blinking and be permanently on. If this is annoying you can disable with `set led off`. To see the EMS messages type `set log v` for verbose logging. 18. And all is not well, check the wiring, make sure serial is off and look at the telnet session for errors. If in doubt, wipe the ESP with `pio run -t erase` and start again with step #3 diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 0c9cbfa54..9e76ed77a 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6b4" +#define MYESP_VERSION "1.1.6" #include #include diff --git a/src/ds18.cpp b/src/ds18.cpp index 08bb6d7ca..6a827970f 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -182,6 +182,7 @@ int16_t DS18::getRawValue(unsigned char index) { } // return real value as a double +// The raw temperature data is in units of sixteenths of a degree, so the value must be divided by 16 in order to convert it to degrees. double DS18::getValue(unsigned char index) { double value = (float)getRawValue(index) / 16.0; return value; diff --git a/src/ems-esp.ino b/src/ems-esp.cpp similarity index 95% rename from src/ems-esp.ino rename to src/ems-esp.cpp index 9376d0687..1312bcdb5 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.cpp @@ -161,56 +161,42 @@ char * _bool_to_char(char * s, uint8_t value) { // convert short (two bytes) to text value // negative values are assumed stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c) -char * _short_to_char(char * s, int16_t value, uint8_t div = 10) { +char * _short_to_char(char * s, int16_t value, uint8_t decimals = 1) { // remove errors on invalid values if (abs(value) >= EMS_VALUE_SHORT_NOTSET) { strlcpy(s, "?", sizeof(s)); return (s); } - if (div != 0) { - char s2[5] = {0}; - // check for negative values - if (value < 0) { - strlcpy(s, "-", 2); - strlcat(s, itoa(abs(value) / div, s2, 10), 5); - } else { - strlcpy(s, itoa(value / div, s2, 10), 5); - } - strlcat(s, ".", sizeof(s)); - strlcat(s, itoa(abs(value) % div, s2, 10), 5); - } else { + if (decimals == 0) { itoa(value, s, 10); + return (s); } + + // floating point + char s2[5] = {0}; + // check for negative values + if (value < 0) { + strlcpy(s, "-", 2); + value = abs(value); + } + strlcpy(s, itoa(value / (decimals * 10), s2, 10), 5); + strlcat(s, ".", sizeof(s)); + strlcat(s, itoa(value % (decimals * 10), s2, 10), 5); + return s; } -// convert int (single byte) to text value -char * _int_to_char(char * s, uint8_t value, uint8_t div = 0) { - if (value == EMS_VALUE_INT_NOTSET) { - strlcpy(s, "?", sizeof(s)); - } else { - if (div != 0) { - char s2[5] = {0}; - strlcpy(s, itoa(value / div, s2, 10), 5); - strlcat(s, ".", sizeof(s)); - strlcat(s, itoa(value % div, s2, 10), 5); - } else { - itoa(value, s, 10); - } - } - return s; -} - -// takes an int value (1 byte), converts to a fraction -void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div = 0) { +// takes a short value (2 bytes), converts to a fraction +// most values stored a s short are either *10 or *100 +void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals = 1) { char buffer[200] = {0}; char s[20] = {0}; strlcpy(buffer, " ", sizeof(buffer)); strlcat(buffer, prefix, sizeof(buffer)); strlcat(buffer, ": ", sizeof(buffer)); - strlcat(buffer, _int_to_char(s, value, div), sizeof(buffer)); + strlcat(buffer, _short_to_char(s, value, decimals), sizeof(buffer)); if (postfix != NULL) { strlcat(buffer, " ", sizeof(buffer)); @@ -220,15 +206,49 @@ void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, u myDebug(buffer); } -// takes a short value (2 bytes), converts to a fraction -void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t div = 10) { +// convert int (single byte) to text value +char * _int_to_char(char * s, uint8_t value, uint8_t div = 1) { + if (value == EMS_VALUE_INT_NOTSET) { + strlcpy(s, "?", sizeof(s)); + return (s); + } + + char s2[5] = {0}; + + switch (div) { + case 1: + itoa(value, s, 10); + break; + + case 2: + strlcpy(s, itoa(value >> 1, s2, 10), 5); + strlcat(s, ".", sizeof(s)); + strlcat(s, ((value & 0x01) ? "5" : "0"), 5); + break; + + case 10: + strlcpy(s, itoa(value / 10, s2, 10), 5); + strlcat(s, ".", sizeof(s)); + strlcat(s, itoa(value % 10, s2, 10), 5); + break; + + default: + itoa(value, s, 10); + break; + } + + return s; +} + +// takes an int value (1 byte), converts to a fraction +void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div = 1) { char buffer[200] = {0}; char s[20] = {0}; strlcpy(buffer, " ", sizeof(buffer)); strlcat(buffer, prefix, sizeof(buffer)); strlcat(buffer, ": ", sizeof(buffer)); - strlcat(buffer, _short_to_char(s, value, div), sizeof(buffer)); + strlcat(buffer, _int_to_char(s, value, div), sizeof(buffer)); if (postfix != NULL) { strlcat(buffer, " ", sizeof(buffer)); @@ -422,11 +442,16 @@ void showInfo() { if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_BOSCHEASY)) { // for easy temps are * 100 // also we don't have the time or mode - _renderShortValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 100); - _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 100); - } else { - _renderShortValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 2); + _renderShortValue("Set room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 10); _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 10); + } else { + // because we store in 2 bytes short, when converting to a single byte we'll loose the negative value if its unset + if ((EMS_Thermostat.setpoint_roomTemp <= 0) || (EMS_Thermostat.curr_roomTemp <= 0)) { + EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_INT_NOTSET; + EMS_Thermostat.curr_roomTemp = EMS_VALUE_INT_NOTSET; + } + _renderIntValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 2); // convert to a single byte * 2 + _renderIntValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 10); // is *10 myDebug(" Thermostat time is %02d:%02d:%02d %d/%d/%d", EMS_Thermostat.hour, @@ -451,12 +476,12 @@ void showInfo() { // Dallas if (EMSESP_Status.dallas_sensors != 0) { - char s[80] = {0}; + //char s[80] = {0}; char buffer[128] = {0}; + char valuestr[8] = {0}; // for formatting temp myDebug("%sExternal temperature sensors:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { - snprintf(s, sizeof(s), "Sensor #%d %s", i + 1, ds18.getDeviceString(buffer, i)); - _renderShortValue(s, "C", ds18.getRawValue(i), 16); // divide by 16 + myDebug(" Sensor #%d %s: %s C", i + 1, ds18.getDeviceString(buffer, i), _float_to_char(valuestr, ds18.getValue(i) )); } myDebug(""); // newline } @@ -524,9 +549,8 @@ void publishValues(bool force) { rootBoiler["wWComfort"] = "Intelligent"; } - rootBoiler["wWCurTmp"] = _short_to_char(s, EMS_Boiler.wWCurTmp); - snprintf(s, sizeof(s), "%i.%i", EMS_Boiler.wWCurFlow / 10, EMS_Boiler.wWCurFlow % 10); - rootBoiler["wWCurFlow"] = s; + rootBoiler["wWCurTmp"] = _short_to_char(s, EMS_Boiler.wWCurTmp); + rootBoiler["wWCurFlow"] = _int_to_char(s, EMS_Boiler.wWCurFlow, 10); rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); rootBoiler["curFlowTemp"] = _short_to_char(s, EMS_Boiler.curFlowTemp); rootBoiler["retTemp"] = _short_to_char(s, EMS_Boiler.retTemp); @@ -571,7 +595,7 @@ void publishValues(bool force) { // handle the thermostat values separately if (ems_getThermostatEnabled()) { // only send thermostat values if we actually have them - if ((EMS_Thermostat.curr_roomTemp == 0) || (EMS_Thermostat.setpoint_roomTemp == 0)) + if ((EMS_Thermostat.curr_roomTemp <= 0) || (EMS_Thermostat.setpoint_roomTemp <= 0)) return; // build new json object @@ -579,11 +603,11 @@ void publishValues(bool force) { JsonObject rootThermostat = doc.to(); if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_BOSCHEASY)) { - rootThermostat[THERMOSTAT_CURRTEMP] = _short_to_char(s, EMS_Thermostat.curr_roomTemp, 100); - rootThermostat[THERMOSTAT_SELTEMP] = _short_to_char(s, EMS_Thermostat.setpoint_roomTemp, 100); - } else { + rootThermostat[THERMOSTAT_SELTEMP] = _short_to_char(s, EMS_Thermostat.setpoint_roomTemp, 10); rootThermostat[THERMOSTAT_CURRTEMP] = _short_to_char(s, EMS_Thermostat.curr_roomTemp, 10); - rootThermostat[THERMOSTAT_SELTEMP] = _short_to_char(s, EMS_Thermostat.setpoint_roomTemp, 2); + } else { + rootThermostat[THERMOSTAT_SELTEMP] = _int_to_char(s, EMS_Thermostat.setpoint_roomTemp, 2); + rootThermostat[THERMOSTAT_CURRTEMP] = _int_to_char(s, EMS_Thermostat.curr_roomTemp, 10); } // RC20 has different mode settings @@ -701,6 +725,61 @@ char * _readWord() { return word; } +// publish external dallas sensor temperature values to MQTT +void do_publishSensorValues() { + if (EMSESP_Status.dallas_sensors != 0) { + publishSensorValues(); + } +} + +// call PublishValues without forcing, so using CRC to see if we really need to publish +void do_publishValues() { + // don't publish if we're not connected to the EMS bus + if ((ems_getBusConnected()) && (!myESP.getUseSerial()) && myESP.isMQTTConnected()) { + publishValues(false); + } +} + +// callback to light up the LED, called via Ticker every second +// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off +void do_ledcheck() { + if (EMSESP_Status.led) { + if (ems_getBusConnected()) { + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off + } else { + int state = digitalRead(EMSESP_Status.led_gpio); + digitalWrite(EMSESP_Status.led_gpio, !state); + } + } +} + +// Thermostat scan +void do_scanThermostat() { + if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebug("> Scanning thermostat message type #0x%02X...", scanThermostat_count); + ems_doReadCommand(scanThermostat_count, EMS_Thermostat.type_id); + scanThermostat_count++; + } +} + +// do a system health check every now and then to see if we all connections +void do_systemCheck() { + if ((!ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebug("Error! Unable to read from EMS bus. Retrying in %d seconds...", SYSTEMCHECK_TIME); + } +} + +// force calls to get data from EMS for the types that aren't sent as broadcasts +// only if we have a EMS connection +void do_regularUpdates() { + if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebugLog("Calling scheduled data refresh from EMS devices..."); + ems_getThermostatValues(); + ems_getBoilerValues(); + ems_getOtherValues(); + } +} + // initiate a force scan by sending type read requests from 0 to FF to the thermostat // used to analyze responses for debugging void startThermostatScan(uint8_t start) { @@ -713,6 +792,27 @@ void startThermostatScan(uint8_t start) { scanThermostat.attach(SCANTHERMOSTAT_TIME, do_scanThermostat); } +// turn back on the hot water for the shower +void _showerColdShotStop() { + if (EMSESP_Shower.doingColdShot) { + myDebugLog("[Shower] finished shot of cold. hot water back on"); + ems_setWarmTapWaterActivated(true); + EMSESP_Shower.doingColdShot = false; + showerColdShotStopTimer.detach(); // disable the timer + } +} + +// turn off hot water to send a shot of cold +void _showerColdShotStart() { + if (EMSESP_Status.shower_alert) { + myDebugLog("[Shower] doing a shot of cold water"); + ems_setWarmTapWaterActivated(false); + EMSESP_Shower.doingColdShot = true; + // start the timer for n seconds which will reset the water back to hot + showerColdShotStopTimer.attach(SHOWER_COLDSHOT_DURATION, _showerColdShotStop); + } +} + // callback for loading/saving settings to the file system (SPIFFS) bool FSCallback(MYESP_FSACTION action, const JsonObject json) { bool recreate_config = true; @@ -1235,82 +1335,6 @@ void initEMSESP() { EMSESP_Shower.doingColdShot = false; } -// publish external dallas sensor temperature values to MQTT -void do_publishSensorValues() { - if (EMSESP_Status.dallas_sensors != 0) { - publishSensorValues(); - } -} - -// call PublishValues without forcing, so using CRC to see if we really need to publish -void do_publishValues() { - // don't publish if we're not connected to the EMS bus - if ((ems_getBusConnected()) && (!myESP.getUseSerial()) && myESP.isMQTTConnected()) { - publishValues(false); - } -} - -// callback to light up the LED, called via Ticker every second -// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off -void do_ledcheck() { - if (EMSESP_Status.led) { - if (ems_getBusConnected()) { - digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off - } else { - int state = digitalRead(EMSESP_Status.led_gpio); - digitalWrite(EMSESP_Status.led_gpio, !state); - } - } -} - -// Thermostat scan -void do_scanThermostat() { - if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebug("> Scanning thermostat message type #0x%02X...", scanThermostat_count); - ems_doReadCommand(scanThermostat_count, EMS_Thermostat.type_id); - scanThermostat_count++; - } -} - -// do a system health check every now and then to see if we all connections -void do_systemCheck() { - if ((!ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebug("Error! Unable to read from EMS bus. Retrying in %d seconds...", SYSTEMCHECK_TIME); - } -} - -// force calls to get data from EMS for the types that aren't sent as broadcasts -// only if we have a EMS connection -void do_regularUpdates() { - if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebugLog("Calling scheduled data refresh from EMS devices..."); - ems_getThermostatValues(); - ems_getBoilerValues(); - ems_getOtherValues(); - } -} - -// turn off hot water to send a shot of cold -void _showerColdShotStart() { - if (EMSESP_Status.shower_alert) { - myDebugLog("[Shower] doing a shot of cold water"); - ems_setWarmTapWaterActivated(false); - EMSESP_Shower.doingColdShot = true; - // start the timer for n seconds which will reset the water back to hot - showerColdShotStopTimer.attach(SHOWER_COLDSHOT_DURATION, _showerColdShotStop); - } -} - -// turn back on the hot water for the shower -void _showerColdShotStop() { - if (EMSESP_Shower.doingColdShot) { - myDebugLog("[Shower] finished shot of cold. hot water back on"); - ems_setWarmTapWaterActivated(true); - EMSESP_Shower.doingColdShot = false; - showerColdShotStopTimer.detach(); // disable the timer - } -} - /* * Shower Logic */ diff --git a/src/ems.cpp b/src/ems.cpp index 31cf80478..842780766 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -14,11 +14,6 @@ #include #include // std::list -#define _toByte(i) (data[i]) -#define _toShort(i) ((data[i] << 8) + data[i + 1]) -#define _toLong(i) ((data[i] << 16) + (data[i + 1] << 8) + (data[i + 2])) -#define _bitRead(i, bit) (((data[i]) >> (bit)) & 0x01) - // myESP for logging to telnet and serial #define myDebug(...) myESP.myDebug(__VA_ARGS__) @@ -26,7 +21,15 @@ _EMS_Sys_Status EMS_Sys_Status; // EMS Status CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO queue for Tx send buffer -// callbacks per type +// +// process callbacks per type +// + +// macros used in the _process* functions +#define _toByte(i) (data[i]) +#define _toShort(i) ((data[i] << 8) + data[i + 1]) +#define _toLong(i) ((data[i] << 16) + (data[i + 1] << 8) + (data[i + 2])) +#define _bitRead(i, bit) (((data[i]) >> (bit)) & 0x01) // generic void _process_Version(uint8_t src, uint8_t * data, uint8_t length); @@ -1191,7 +1194,7 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { if (typeFound) { // its a boiler - myDebug("Boiler found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + myDebug("Boiler found. Model %s with TypeID 0x%02X, ProductID %d, Version %s", Boiler_Types[i].model_string, Boiler_Types[i].type_id, product_id, @@ -1200,7 +1203,7 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { // if its a boiler set it // it will take the first one found in the list if ((EMS_Boiler.type_id == EMS_ID_NONE) || (EMS_Boiler.type_id == Boiler_Types[i].type_id)) { - myDebug("* Setting Boiler type to Model %s, TypeID 0x%02X, Product ID %d, Version %s", + myDebug("* Setting Boiler type to Model %s, TypeID 0x%02X, ProductID %d, Version %s", Boiler_Types[i].model_string, Boiler_Types[i].type_id, product_id, @@ -1230,7 +1233,7 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { if (typeFound) { // its a known thermostat if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Thermostat found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + myDebug("Thermostat found. Model %s with TypeID 0x%02X, ProductID %d, Version %s", Thermostat_Types[i].model_string, Thermostat_Types[i].type_id, product_id, @@ -1240,7 +1243,7 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { // if we don't have a thermostat set, use this one if ((EMS_Thermostat.type_id == EMS_ID_NONE) || (EMS_Thermostat.model_id == EMS_MODEL_NONE) || (EMS_Thermostat.type_id == Thermostat_Types[i].type_id)) { - myDebug("* Setting Thermostat type to Model %s, TypeID 0x%02X, Product ID %d, Version %s", + myDebug("* Setting Thermostat type to Model %s, TypeID 0x%02X, ProductID %d, Version %s", Thermostat_Types[i].model_string, Thermostat_Types[i].type_id, product_id, @@ -1272,7 +1275,7 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { } if (typeFound) { - myDebug("Device found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + myDebug("Device found. Model %s with TypeID 0x%02X, ProductID %d, Version %s", Other_Types[i].model_string, Other_Types[i].type_id, product_id, @@ -1289,7 +1292,7 @@ void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { return; } else { - myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", src, product_id, version); + myDebug("Unrecognized device found. TypeID 0x%02X, ProductID %d, Version %s", src, product_id, version); } } @@ -1339,7 +1342,7 @@ void _ems_setThermostatModel(uint8_t thermostat_modelid) { // set the thermostat if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Setting Thermostat. Model %s with TypeID 0x%02X, Product ID %d", + myDebug("Setting Thermostat. Model %s with TypeID 0x%02X, ProductID %d", thermostat_type->model_string, thermostat_type->type_id, thermostat_type->product_id); @@ -1485,7 +1488,7 @@ char * ems_getThermostatDescription(char * buffer) { strlcat(buffer, _hextoa(EMS_Thermostat.type_id, tmp), size); } - strlcat(buffer, " (Product ID:", size); + strlcat(buffer, " (ProductID:", size); strlcat(buffer, itoa(EMS_Thermostat.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Thermostat.version, size); @@ -1522,7 +1525,7 @@ char * ems_getBoilerDescription(char * buffer) { strlcat(buffer, _hextoa(EMS_Boiler.type_id, tmp), size); } - strlcat(buffer, " (Product ID:", size); + strlcat(buffer, " (ProductID:", size); strlcat(buffer, itoa(EMS_Boiler.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Boiler.version, size); @@ -1574,12 +1577,12 @@ void ems_printAllTypes() { myDebug("\nThese %d boiler type devices are in the library:", _Boiler_Types_max); for (i = 0; i < _Boiler_Types_max; i++) { - myDebug(" %s, type ID:0x%02X Product ID:%d", Boiler_Types[i].model_string, Boiler_Types[i].type_id, Boiler_Types[i].product_id); + myDebug(" %s, type ID:0x%02X ProductID:%d", Boiler_Types[i].model_string, Boiler_Types[i].type_id, Boiler_Types[i].product_id); } myDebug("\nThese %d EMS devices are in the library:", _Other_Types_max); for (i = 0; i < _Other_Types_max; i++) { - myDebug(" %s, type ID:0x%02X Product ID:%d", Other_Types[i].model_string, Other_Types[i].type_id, Other_Types[i].product_id); + myDebug(" %s, type ID:0x%02X ProductID:%d", Other_Types[i].model_string, Other_Types[i].type_id, Other_Types[i].product_id); } myDebug("\nThese telegram type IDs are recognized for the selected boiler:"); @@ -1591,7 +1594,7 @@ void ems_printAllTypes() { myDebug("\nThese %d thermostats models are supported:", _Thermostat_Types_max); for (i = 0; i < _Thermostat_Types_max; i++) { - myDebug(" %s, type ID:0x%02X Product ID:%d Read/Write support:%c%c", + myDebug(" %s, type ID:0x%02X ProductID:%d Read/Write support:%c%c", Thermostat_Types[i].model_string, Thermostat_Types[i].type_id, Thermostat_Types[i].product_id, @@ -1940,7 +1943,7 @@ void ems_setWarmTapWaterActivated(bool activated) { } /* - * Start up sequence for UBA Master + * Start up sequence for UBA Master, hopefully to initialize a handshake * Still experimental */ void ems_startupTelegrams() { diff --git a/src/version.h b/src/version.h index a8377ec7c..4618d14cc 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.6.0b5" +#define APP_VERSION "1.6.0b6" #define APP_HOSTNAME "ems-esp" From b89c8dd9fab8be53d9dd05ab0b72ff1a3e3fc6f2 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 12:38:46 +0100 Subject: [PATCH 52/59] fixed SM10 pump on/off location --- src/ems.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ems.cpp b/src/ems.cpp index 842780766..2c555573f 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -1129,7 +1129,7 @@ void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { EMS_Other.SM10collectorTemp = _toShort(2); // collector temp from SM10, is *10 EMS_Other.SM10bottomTemp = _toShort(5); // bottom temp from SM10, is *10 EMS_Other.SM10pumpModulation = _toByte(4); // modulation solar pump - EMS_Other.SM10pump = _bitRead(5, 1); // active if bit 1 is set (to 1) + EMS_Other.SM10pump = _bitRead(7, 1); // active if bit 1 is set (to 1) EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } From 42555469baf51ea57ed5cd5b55b43fb496833823 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 15:54:53 +0100 Subject: [PATCH 53/59] comment change --- src/ems.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ems.cpp b/src/ems.cpp index 2c555573f..b62ef51fe 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -1129,7 +1129,7 @@ void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { EMS_Other.SM10collectorTemp = _toShort(2); // collector temp from SM10, is *10 EMS_Other.SM10bottomTemp = _toShort(5); // bottom temp from SM10, is *10 EMS_Other.SM10pumpModulation = _toByte(4); // modulation solar pump - EMS_Other.SM10pump = _bitRead(7, 1); // active if bit 1 is set (to 1) + EMS_Other.SM10pump = _bitRead(7, 1); // active if bit 1 is set EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } From 578783382a07d777731fb2d237e5baea3e6e449f Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 15:55:20 +0100 Subject: [PATCH 54/59] length is single byte as buffer should not exceed 32 --- src/emsuart.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emsuart.cpp b/src/emsuart.cpp index fb4827ea2..788d9bd46 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -24,8 +24,8 @@ os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue // Important: do not use ICACHE_FLASH_ATTR ! // static void emsuart_rx_intr_handler(void * para) { - static uint16_t length; - static uint8_t uart_buffer[EMS_MAXBUFFERSIZE]; + static uint8_t length; + static uint8_t uart_buffer[EMS_MAXBUFFERSIZE]; // is a new buffer? if so init the thing for a new telegram if (EMS_Sys_Status.emsRxStatus == EMS_RX_STATUS_IDLE) { From 303e3adc81a1e610c80c7641879caaaf2091ca87 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 16:26:45 +0100 Subject: [PATCH 55/59] test code for duplicate issue --- src/ems.cpp | 22 +++++++++++++++++----- src/ems.h | 45 +++++++++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index b62ef51fe..1e857dc8f 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -136,9 +136,9 @@ uint8_t _Other_Types_max = ArraySize(Other_Types); // number of other uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types // these structs contain the data we store from the Boiler and Thermostat -_EMS_Boiler EMS_Boiler; // for boiler +_EMS_Boiler EMS_Boiler; // for boiler _EMS_Thermostat EMS_Thermostat; // for thermostat -_EMS_Other EMS_Other; // for other known EMS devices +_EMS_Other EMS_Other; // for other known EMS devices // CRC lookup table with poly 12 for faster checking const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, @@ -577,13 +577,25 @@ void _createValidate() { EMS_TxQueue.unshift(new_EMS_TxTelegram); // add back to queue making it first to be picked up next (FIFO) } + +/* + * Entry point TODO: tidy up + */ +void ems_parseTelegram(uint8_t * telegram, uint8_t length) { + _ems_readTelegram(telegram, length); + // now clear it just be safe + for (uint8_t i = 0; i < EMS_MAXBUFFERSIZE; i++) { + telegram[i] = 0x00; + } +} + /** * the main logic that parses the telegram message, triggered by an interrupt in emsuart.cpp * length is only data bytes, excluding the BRK * Read commands are asynchronous as they're handled by the interrupt * When we receive a Poll Request we need to send any Tx packages quickly within a 200ms window */ -void ems_parseTelegram(uint8_t * telegram, uint8_t length) { +void _ems_readTelegram(uint8_t * telegram, uint8_t length) { // check if we just received a single byte // it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) // or either a return code like 0x01 or 0x04 from the last Write command @@ -991,7 +1003,7 @@ void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length) { // read the service code / installation status as appears on the display EMS_Boiler.serviceCodeChar[0] = char(_toByte(18)); // ascii character 1 EMS_Boiler.serviceCodeChar[1] = char(_toByte(19)); // ascii character 2 - EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string + EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string // read error code EMS_Boiler.serviceCode = _toShort(20); @@ -1036,7 +1048,7 @@ void _process_RC10StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { */ void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC20StatusMessage_setpoint); // is * 2 - EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC20StatusMessage_curr); // is * 10 + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC20StatusMessage_curr); // is * 10 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } diff --git a/src/ems.h b/src/ems.h index 8211d76e8..e1e97f33a 100644 --- a/src/ems.h +++ b/src/ems.h @@ -174,8 +174,8 @@ typedef struct { // UBAParameterWW // UBAMonitorFast uint8_t selFlowTemp; // Selected flow temperature - int16_t curFlowTemp; // Current flow temperature - int16_t retTemp; // Return temperature + int16_t curFlowTemp; // Current flow temperature + int16_t retTemp; // Return temperature uint8_t burnGas; // Gas on/off uint8_t fanWork; // Fan on/off uint8_t ignWork; // Ignition on/off @@ -190,15 +190,15 @@ typedef struct { // UBAParameterWW uint16_t serviceCode; // error/service code // UBAMonitorSlow - int16_t extTemp; // Outside temperature - int16_t boilTemp; // Boiler temperature + int16_t extTemp; // Outside temperature + int16_t boilTemp; // Boiler temperature uint8_t pumpMod; // Pump modulation uint32_t burnStarts; // # burner starts uint32_t burnWorkMin; // Total burner operating time uint32_t heatWorkMin; // Total heat operating time // UBAMonitorWWMessage - int16_t wWCurTmp; // Warm Water current temperature: + int16_t wWCurTmp; // Warm Water current temperature: uint32_t wWStarts; // Warm Water # starts uint32_t wWWorkM; // Warm Water # minutes uint8_t wWOneTime; // Warm Water one time function on/off @@ -227,31 +227,31 @@ typedef struct { // UBAParameterWW */ typedef struct { // SM10 Solar Module - SM10Monitor - bool SM10; // set true if there is a SM10 available + bool SM10; // set true if there is a SM10 available int16_t SM10collectorTemp; // collector temp from SM10 int16_t SM10bottomTemp; // bottom temp from SM10 - uint8_t SM10pumpModulation; // modulation solar pump - uint8_t SM10pump; // pump active + uint8_t SM10pumpModulation; // modulation solar pump + uint8_t SM10pump; // pump active } _EMS_Other; // Thermostat data typedef struct { - uint8_t type_id; // the type ID of the thermostat - uint8_t model_id; // which Thermostat type - uint8_t product_id; - bool read_supported; - bool write_supported; - char version[10]; + uint8_t type_id; // the type ID of the thermostat + uint8_t model_id; // which Thermostat type + uint8_t product_id; + bool read_supported; + bool write_supported; + char version[10]; int16_t setpoint_roomTemp; // current set temp int16_t curr_roomTemp; // current room temp - uint8_t mode; // 0=low, 1=manual, 2=auto - bool day_mode; // 0=night, 1=day - uint8_t hour; - uint8_t minute; - uint8_t second; - uint8_t day; - uint8_t month; - uint8_t year; + uint8_t mode; // 0=low, 1=manual, 2=auto + bool day_mode; // 0=night, 1=day + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t day; + uint8_t month; + uint8_t year; } _EMS_Thermostat; // call back function signature for processing telegram types @@ -315,6 +315,7 @@ int _ems_findBoilerModel(uint8_t model_id); bool _ems_setModel(uint8_t model_id); void _ems_setThermostatModel(uint8_t thermostat_modelid); void _removeTxQueue(); +void _ems_readTelegram(uint8_t * telegram, uint8_t length); // global so can referenced in other classes extern _EMS_Sys_Status EMS_Sys_Status; From 7af42ac8ae375f22d6051d66b4abf00a7373576c Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 19:45:44 +0100 Subject: [PATCH 56/59] mention rename from .ino file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0565c2cd..d54127a5b 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ Every telegram sent is echo'd back to Rx, along the same Bus used for all Rx/Tx `ems.cpp` is the logic to read the EMS data packets (telegrams), validates them and process them based on the type. -`ems-esp.ino` is the Arduino code for the ESP8266 that kicks it all off. This is where we have specific logic such as the code to monitor and alert on the Shower timer and light up the LEDs. +`ems-esp.cpp` is the Arduino code for the ESP8266 that kicks it all off. This is where we have specific logic such as the code to monitor and alert on the Shower timer and light up the LEDs. `my_config.h` has all the custom settings tailored to your environment. Specific values here are also stored in the ESP's SPIFFs (File system). From 8b0d91a9b816f963793c126fbff32b321451bf31 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 19:45:59 +0100 Subject: [PATCH 57/59] ignore null telegrams --- src/ems.cpp | 25 +++++++++++++------------ src/emsuart.cpp | 9 ++------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/ems.cpp b/src/ems.cpp index 1e857dc8f..2e640915c 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -579,33 +579,34 @@ void _createValidate() { /* - * Entry point TODO: tidy up + * Entry point triggered by an interrupt in emsuart.cpp + * length is only data bytes, excluding the BRK + * Read commands are asynchronous as they're handled by the interrupt + * When a telegram is processed we forcefully erase it from the stack to prevent overflow */ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { - _ems_readTelegram(telegram, length); - // now clear it just be safe - for (uint8_t i = 0; i < EMS_MAXBUFFERSIZE; i++) { - telegram[i] = 0x00; + if ((length != 0) && (telegram[0] != 0x00)) { + _ems_readTelegram(telegram, length); } + // now clear the Rx buffer just be safe and prevent duplicates + for (uint8_t i = 0; i < EMS_MAXBUFFERSIZE; telegram[i++] = 0x00) + ; } /** - * the main logic that parses the telegram message, triggered by an interrupt in emsuart.cpp - * length is only data bytes, excluding the BRK - * Read commands are asynchronous as they're handled by the interrupt + * the main logic that parses the telegram message * When we receive a Poll Request we need to send any Tx packages quickly within a 200ms window */ void _ems_readTelegram(uint8_t * telegram, uint8_t length) { - // check if we just received a single byte - // it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) - // or either a return code like 0x01 or 0x04 from the last Write command - // create the Rx package static _EMS_RxTelegram EMS_RxTelegram; EMS_RxTelegram.length = length; EMS_RxTelegram.telegram = telegram; EMS_RxTelegram.timestamp = millis(); + // check if we just received a single byte + // it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) + // or either a return code like 0x01 or 0x04 from the last Write command if (length == 1) { uint8_t value = telegram[0]; // 1st byte of data package diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 788d9bd46..65b479b0b 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -71,14 +71,9 @@ static void emsuart_rx_intr_handler(void * para) { * The full buffer is sent to the ems_parseTelegram() function in ems.cpp. */ static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) { - // get next free EMS Receive buffer _EMSRxBuf * pCurrent = pEMSRxBuf; - pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; - - // transmit EMS buffer, excluding the BRK - if (pCurrent->writePtr > 1) { - ems_parseTelegram((uint8_t *)pCurrent->buffer, (pCurrent->writePtr) - 1); - } + ems_parseTelegram((uint8_t *)pCurrent->buffer, (pCurrent->writePtr) - 1); // transmit EMS buffer, excluding the BRK + pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; // next free EMS Receive buffer } /* From f87c5a3d667fc8e8ba618355fcafa4f9fed15080 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 24 Mar 2019 21:21:44 +0100 Subject: [PATCH 58/59] 1.7 alpha --- CHANGELOG.md | 24 +- LICENSE | 843 +++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- platformio.ini-example | 13 +- src/ems-esp.cpp | 16 +- src/version.h | 2 +- 6 files changed, 870 insertions(+), 30 deletions(-) create mode 100644 LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md index d651484c6..35df4f9c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,34 +5,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.6.0 dev] 2019-03-23 +## [1.7.0 dev] 2019-03- ### Added -- system command to show ESP8266 stats -- crash command to see stack of last system crash, with .py files to track stack dump (compile with -DCRASH) +- EMS Plus support + +## [1.6.0] 2019-03-24 + +### Added + +- `system` command to show ESP8266 stats +- `crash` command to see stack of last system crash, with .py files to track stack dump (compile with `-DCRASH`) - publish dallas external temp sensors to MQTT (thanks @JewelZB) - shower timer and shower alert options available via set commands - added support for warm water modes Hot, Comfort and Intelligent [(issue 67)](https://github.com/proddy/EMS-ESP/issues/67) -- added 'set publish_time' to set how often to publish MQTT +- added `set publish_time` to set how often to publish MQTT - support for SM10 Solar Module including MQTT [(issue 77)](https://github.com/proddy/EMS-ESP/issues/77) -- 'refresh' command to force a fetch of all known data from the connected EMS devices +- `refresh` command to force a fetch of all known data from the connected EMS devices ### Fixed - incorrect rendering of null temperature values (the -3200 degrees issue) - OTA is more stable -- Added a hack to overcome WiFi power issues in esp core 2.5.0 libraries causing constant re-connects +- Added a hack to overcome WiFi power issues in arduino core 2.5.0 libraries causing constant wifi re-connects - Performance issues with telnet output ### Changed - included various fixes and suggestions from @nomis - upgraded MyESP library with many optimizations -- test_mode renamed to silent_mode -- 'set wifi' replaced with 'set wifi_ssid and set wifi_password' to allow values with spaces +- `test_mode` renamed to `silent_mode` +- `set wifi` replaced with `set wifi_ssid` and `set wifi_password` to allow values with spaces - EMS values are stored in the raw format and only converted to strings when displayed or published, removing the need for parsing floats -- All temps are to one decimal place [(issue 79)](https://github.com/proddy/EMS-ESP/issues/79) +- All floating point temperatures are to one decimal place [(issue 79)](https://github.com/proddy/EMS-ESP/issues/79) ## [1.5.6] 2019-03-09 diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..98cee45b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,843 @@ +<<<<<<< HEAD + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +======= + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. +>>>>>>> d3a07d22b59af38c2308d6a14bfd043dec282dc3 diff --git a/README.md b/README.md index d54127a5b..4aba19f25 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ EMS-ESP is a project to build an electronic controller circuit using an Espressi There are 3 parts to this project, first the design of the circuit, secondly the code for the ESP8266 microcontroller firmware with telnet and MQTT support, and lastly an example configuration for Home Assistant to monitor the data and issue direct commands via a MQTT broker. [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b8880625bdf841d4adb2829732030887)](https://app.codacy.com/app/proddy/EMS-ESP?utm_source=github.com&utm_medium=referral&utm_content=proddy/EMS-ESP&utm_campaign=Badge_Grade_Settings) -[![version](https://img.shields.io/badge/version-1.6.0-brightgreen.svg)](CHANGELOG.md) +[![version](https://img.shields.io/badge/version-1.7.0-brightgreen.svg)](CHANGELOG.md) - [EMS-ESP](#ems-esp) - [Introduction](#introduction) diff --git a/platformio.ini-example b/platformio.ini-example index d6416d4db..0a597bab5 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -3,17 +3,11 @@ env_default = d1_mini [common] -platform_def = espressif8266 -platform_180 = espressif8266@1.8.0 -platform = ${common.platform_def} -;platform = ${common.platform_180} - flash_mode = dout -; for production build_flags = -g -w -; for debug +; for debug use these... ; build_flags = -g -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH wifi_settings = @@ -31,17 +25,16 @@ lib_deps = [env:d1_mini] board = d1_mini -platform = ${common.platform} +platform = espressif8266 framework = arduino lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} ${common.wifi_settings} board_build.flash_mode = ${common.flash_mode} upload_speed = 921600 monitor_speed = 115200 - ; for OTA comment out these sections ;upload_protocol = espota ;upload_port = ems-esp.local -;upload_port = +;upload_port = diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index 1312bcdb5..b03ef638b 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -312,7 +312,7 @@ void showInfo() { } myDebug(" LED is %s, Silent mode is %s", EMSESP_Status.led ? "on" : "off", EMSESP_Status.silent_mode ? "on" : "off"); - myDebug(" # connected Dallas temperature sensors=%d", EMSESP_Status.dallas_sensors); + myDebug(" %d external temperature sensor%s connected", EMSESP_Status.dallas_sensors, (EMSESP_Status.dallas_sensors > 1) ? "s" : ""); myDebug(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s", (ems_getThermostatEnabled() ? "enabled" : "disabled"), @@ -329,7 +329,6 @@ void showInfo() { EMS_Sys_Status.emxCrcErr); myDebug(""); - myDebug("%sBoiler stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); // version details @@ -422,10 +421,9 @@ void showInfo() { EMS_Boiler.UBAuptime % 60); } - myDebug(""); // newline - // For SM10 Solar Module if (EMS_Other.SM10) { + myDebug(""); // newline myDebug("%sSolar Module stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); _renderShortValue(" Collector temperature", "C", EMS_Other.SM10collectorTemp); _renderShortValue(" Bottom temperature", "C", EMS_Other.SM10bottomTemp); @@ -433,10 +431,9 @@ void showInfo() { _renderBoolValue(" Pump active", EMS_Other.SM10pump); } - myDebug(""); // newline - // Thermostat stats if (ems_getThermostatEnabled()) { + myDebug(""); // newline myDebug("%sThermostat stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug(" Thermostat type: %s", ems_getThermostatDescription(buffer_type)); if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_BOSCHEASY)) { @@ -471,23 +468,24 @@ void showInfo() { myDebug(" Mode is set to ?"); } } + myDebug(""); // newline } // Dallas if (EMSESP_Status.dallas_sensors != 0) { - //char s[80] = {0}; + myDebug(""); // newline char buffer[128] = {0}; char valuestr[8] = {0}; // for formatting temp myDebug("%sExternal temperature sensors:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { - myDebug(" Sensor #%d %s: %s C", i + 1, ds18.getDeviceString(buffer, i), _float_to_char(valuestr, ds18.getValue(i) )); + myDebug(" Sensor #%d %s: %s C", i + 1, ds18.getDeviceString(buffer, i), _float_to_char(valuestr, ds18.getValue(i))); } - myDebug(""); // newline } // show the Shower Info if (EMSESP_Status.shower_timer) { + myDebug(""); // newline myDebug("%sShower stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug(" Shower is %s", (EMSESP_Shower.showerOn ? "running" : "off")); } diff --git a/src/version.h b/src/version.h index 4618d14cc..69498c119 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.6.0b6" +#define APP_VERSION "1.7.0b1" #define APP_HOSTNAME "ems-esp" From b31f3118d1440d090c0f68547035047a1918f349 Mon Sep 17 00:00:00 2001 From: proddy Date: Thu, 4 Apr 2019 21:02:20 +0200 Subject: [PATCH 59/59] prepare for ems plujs updates --- CHANGELOG.md | 4 ++-- lib/MyESP/MyESP.cpp | 45 ++++++++++++++++++++--------------- lib/MyESP/MyESP.h | 30 +++++++++++------------ lib/TelnetSpy/TelnetSpy.cpp | 3 +++ src/ds18.cpp | 2 +- src/ems-esp.cpp | 47 +++++++++++++++++++++++-------------- src/ems.cpp | 38 +++++++++++++++++------------- src/ems.h | 8 +++---- src/ems_devices.h | 1 + src/emsuart.cpp | 1 + src/my_config.h | 1 + src/version.h | 2 +- 12 files changed, 107 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35df4f9c2..6df4f4787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.7.0 dev] 2019-03- +## [1.6.1 dev] 2019-03- ### Added -- EMS Plus support +- Buderus Logamax plus ## [1.6.0] 2019-03-24 diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index 0072d3822..ed852d840 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -155,8 +155,6 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { // finally if we don't want Serial anymore, turn it off if (!_use_serial) { myDebug_P(PSTR("Disabling serial port")); - Serial.flush(); - Serial.end(); SerialAndTelnet.setSerial(NULL); } else { myDebug_P(PSTR("Using serial port output")); @@ -679,6 +677,19 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) return ok; } +// force the serial on/off +void MyESP::setUseSerial(bool toggle) { + //(void)fs_saveConfig(); // save the setting for next reboot + + if (toggle) { + SerialAndTelnet.setSerial(&Serial); + _use_serial = true; + } else { + SerialAndTelnet.setSerial(NULL); + _use_serial = false; + } +} + void MyESP::_telnetCommand(char * commandLine) { char * str = commandLine; bool state = false; @@ -792,14 +803,14 @@ String MyESP::_buildTime() { } // returns system uptime in seconds - copied for espurna. see (c) -unsigned long MyESP::_getUptime() { - static unsigned long last_uptime = 0; +uint32_t MyESP::_getUptime() { + static uint32_t last_uptime = 0; static unsigned char uptime_overflows = 0; if (millis() < last_uptime) ++uptime_overflows; - last_uptime = millis(); - unsigned long uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); + last_uptime = millis(); + uint32_t uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); return uptime_seconds; } @@ -884,9 +895,7 @@ void MyESP::showSystemStats() { myDebug_P(PSTR(" [MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); myDebug_P(PSTR(" [MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); myDebug_P(PSTR(" [MEM] Free Heap: %d"), ESP.getFreeHeap()); -#if defined(ESP8266) - myDebug_P(PSTR(" [MEM] Stack: %d"), ESP.getFreeContStack()); -#endif + myDebug_P(PSTR("")); } @@ -1019,8 +1028,8 @@ void MyESP::setMQTT(const char * mqtt_host, const char * mqtt_username, const char * mqtt_password, const char * mqtt_base, - unsigned long mqtt_keepalive, - unsigned char mqtt_qos, + uint32_t mqtt_keepalive, + uint8_t mqtt_qos, bool mqtt_retain, const char * mqtt_will_topic, const char * mqtt_will_online_payload, @@ -1155,7 +1164,7 @@ bool MyESP::_fs_loadConfig() { // Deserialize the JSON document DeserializationError error = deserializeJson(doc, configFile); if (error) { - Serial.println(F("[FS] Failed to read file")); + myDebug_P(PSTR("[FS] Failed to read config file")); return false; } @@ -1264,13 +1273,13 @@ uint16_t MyESP::getSystemLoadAverage() { // calculate load average void MyESP::_calculateLoad() { - static unsigned long last_loadcheck = 0; - static unsigned long load_counter_temp = 0; + static uint32_t last_loadcheck = 0; + static uint32_t load_counter_temp = 0; load_counter_temp++; if (millis() - last_loadcheck > LOADAVG_INTERVAL) { - static unsigned long load_counter = 0; - static unsigned long load_counter_max = 1; + static uint32_t load_counter = 0; + static uint32_t load_counter_max = 1; load_counter = load_counter_temp; load_counter_temp = 0; @@ -1478,7 +1487,7 @@ void MyESP::begin(const char * app_hostname, const char * app_name, const char * _eeprom_setup(); // set up eeprom for storing crash data _fs_setup(); // SPIFFS setup, do this first to get values _wifi_setup(); // WIFI setup - _ota_setup(); + _ota_setup(); // init OTA } /* @@ -1490,12 +1499,10 @@ void MyESP::loop() { jw.loop(); // WiFi - /* // do nothing else until we've got a wifi connection if (WiFi.getMode() & WIFI_AP) { return; } - */ ArduinoOTA.handle(); // OTA _mqttConnect(); // MQTT diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 9e76ed77a..fc7a7a7c5 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyEMS_h #define MyEMS_h -#define MYESP_VERSION "1.1.6" +#define MYESP_VERSION "1.1.7" #include #include @@ -50,7 +50,6 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); #define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection #define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt #define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most -#define MQTT_MAX_SIZE 600 // max length of MQTT message #define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT message // Internal MQTT events @@ -155,7 +154,7 @@ class MyESP { const char * mqtt_username, const char * mqtt_password, const char * mqtt_base, - unsigned long mqtt_keepalive, + uint32_t mqtt_keepalive, unsigned char mqtt_qos, bool mqtt_retain, const char * mqtt_will_topic, @@ -171,6 +170,7 @@ class MyESP { void myDebug_P(PGM_P format_P, ...); void setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback); bool getUseSerial(); + void setUseSerial(bool toggle); // FS void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); @@ -195,7 +195,7 @@ class MyESP { private: // mqtt AsyncMqttClient mqttClient; - unsigned long _mqtt_reconnect_delay; + uint32_t _mqtt_reconnect_delay; void _mqttOnMessage(char * topic, char * payload, size_t len); void _mqttConnect(); void _mqtt_setup(); @@ -207,14 +207,14 @@ class MyESP { char * _mqtt_username; char * _mqtt_password; char * _mqtt_base; - unsigned long _mqtt_keepalive; - unsigned char _mqtt_qos; + uint32_t _mqtt_keepalive; + uint8_t _mqtt_qos; bool _mqtt_retain; char * _mqtt_will_topic; char * _mqtt_will_online_payload; char * _mqtt_will_offline_payload; char * _mqtt_topic; - unsigned long _mqtt_last_connection; + uint32_t _mqtt_last_connection; bool _mqtt_connecting; // wifi @@ -264,14 +264,14 @@ class MyESP { void _printSetCommands(); // general - char * _app_hostname; - char * _app_name; - char * _app_version; - char * _boottime; - bool _suspendOutput; - bool _use_serial; - unsigned long _getUptime(); - String _buildTime(); + char * _app_hostname; + char * _app_name; + char * _app_version; + char * _boottime; + bool _suspendOutput; + bool _use_serial; + uint32_t _getUptime(); + String _buildTime(); // load average (0..100) void _calculateLoad(); diff --git a/lib/TelnetSpy/TelnetSpy.cpp b/lib/TelnetSpy/TelnetSpy.cpp index ea1762fa4..482a0fa97 100644 --- a/lib/TelnetSpy/TelnetSpy.cpp +++ b/lib/TelnetSpy/TelnetSpy.cpp @@ -343,9 +343,11 @@ void TelnetSpy::flush(void) { #ifdef ESP8266 void TelnetSpy::begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin) { + if (usedSer) { usedSer->begin(baud, config, mode, tx_pin); } + started = true; } @@ -433,6 +435,7 @@ TelnetSpy::operator bool() const { } void TelnetSpy::setDebugOutput(bool en) { + debugOutput = en; // TODO: figure out how to disable system printing for the ESP32 diff --git a/src/ds18.cpp b/src/ds18.cpp index 6a827970f..547eb88cd 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -50,7 +50,7 @@ uint8_t DS18::setup(uint8_t gpio, bool parasite) { // scan every 2 seconds void DS18::loop() { - static unsigned long last = 0; + static uint32_t last = 0; if (millis() - last < DS18_READ_INTERVAL) return; last = millis(); diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index b03ef638b..bf683b5fd 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -63,8 +63,8 @@ Ticker showerColdShotStopTimer; #define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water typedef struct { - unsigned long timestamp; // for internal timings, via millis() - uint8_t dallas_sensors; // count of dallas sensors + uint32_t timestamp; // for internal timings, via millis() + uint8_t dallas_sensors; // count of dallas sensors // custom params bool shower_timer; // true if we want to report back on shower times @@ -78,11 +78,11 @@ typedef struct { } _EMSESP_Status; typedef struct { - bool showerOn; - unsigned long timerStart; // ms - unsigned long timerPause; // ms - unsigned long duration; // ms - bool doingColdShot; // true if we've just sent a jolt of cold water + bool showerOn; + uint32_t timerStart; // ms + uint32_t timerPause; // ms + uint32_t duration; // ms + bool doingColdShot; // true if we've just sent a jolt of cold water } _EMSESP_Shower; command_t PROGMEM project_cmds[] = { @@ -312,7 +312,9 @@ void showInfo() { } myDebug(" LED is %s, Silent mode is %s", EMSESP_Status.led ? "on" : "off", EMSESP_Status.silent_mode ? "on" : "off"); - myDebug(" %d external temperature sensor%s connected", EMSESP_Status.dallas_sensors, (EMSESP_Status.dallas_sensors > 1) ? "s" : ""); + if (EMSESP_Status.dallas_sensors > 0) { + myDebug(" %d external temperature sensor%s connected", EMSESP_Status.dallas_sensors, (EMSESP_Status.dallas_sensors == 1) ? "" : "s"); + } myDebug(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s", (ems_getThermostatEnabled() ? "enabled" : "disabled"), @@ -321,12 +323,23 @@ void showInfo() { ((EMSESP_Status.shower_alert) ? "enabled" : "disabled")); myDebug("\n%sEMS Bus stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" Bus Connected=%s, Tx is %s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d", - (ems_getBusConnected() ? "yes" : "no"), - (ems_getTxCapable() ? "active" : "not active"), - EMS_Sys_Status.emsRxPgks, - EMS_Sys_Status.emsTxPkgs, - EMS_Sys_Status.emxCrcErr); + + if (ems_getBusConnected()) { + myDebug(" Bus is connected"); + + myDebug(" Rx: Poll=%d ms, # Rx telegrams read=%d, # Crc Errors=%d", + ems_getPollFrequency(), + EMS_Sys_Status.emsRxPgks, + EMS_Sys_Status.emxCrcErr); + + if (ems_getTxCapable()) { + myDebug(" Tx: available, # Tx telegrams sent=%d", EMS_Sys_Status.emsTxPkgs); + } else { + myDebug(" Tx: no signal"); + } + } else { + myDebug(" No connection can be made to the EMS bus"); + } myDebug(""); myDebug("%sBoiler stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); @@ -493,8 +506,8 @@ void showInfo() { // send all dallas sensor values as a JSON package to MQTT void publishSensorValues() { - StaticJsonDocument doc; - JsonObject sensors = doc.to(); + StaticJsonDocument<200> doc; + JsonObject sensors = doc.to(); bool hasdata = false; char label[8] = {0}; @@ -511,7 +524,7 @@ void publishSensorValues() { } if (hasdata) { - char data[MQTT_MAX_SIZE] = {0}; + char data[200] = {0}; serializeJson(doc, data, sizeof(data)); myESP.mqttPublish(TOPIC_EXTERNAL_SENSORS, data); } diff --git a/src/ems.cpp b/src/ems.cpp index 2e640915c..5425f6d63 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -157,9 +157,9 @@ const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0xCD, 0xCF, 0xC1, 0xC3, 0xC5, 0xC7, 0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5, 0xF7, 0xE9, 0xEB, 0xED, 0xEF, 0xE1, 0xE3, 0xE5, 0xE7}; -const uint8_t TX_WRITE_TIMEOUT_COUNT = 2; // 3 retries before timeout -const unsigned long EMS_BUS_TIMEOUT = 15000; // timeout in ms before recognizing the ems bus is offline (15 seconds) -const unsigned long EMS_POLL_TIMEOUT = 5000; // timeout in ms before recognizing the ems bus is offline (5 seconds) +const uint8_t TX_WRITE_TIMEOUT_COUNT = 2; // 3 retries before timeout +const uint32_t EMS_BUS_TIMEOUT = 15000; // timeout in ms before recognizing the ems bus is offline (15 seconds) +const uint32_t EMS_POLL_TIMEOUT = 5000; // timeout in ms before recognizing the ems bus is offline (5 seconds) // init stats and counters and buffers // uses -255 or 255 for values that haven't been set yet (EMS_VALUE_INT_NOTSET and EMS_VALUE_FLOAT_NOTSET) @@ -176,7 +176,7 @@ void ems_init() { EMS_Sys_Status.emsRxTimestamp = 0; EMS_Sys_Status.emsTxCapable = false; EMS_Sys_Status.emsTxDisabled = false; - EMS_Sys_Status.emsPollTimestamp = 0; + EMS_Sys_Status.emsPollFrequency = 0; EMS_Sys_Status.txRetryCount = 0; // thermostat @@ -302,8 +302,12 @@ void ems_setTxDisabled(bool b) { EMS_Sys_Status.emsTxDisabled = b; } +uint32_t ems_getPollFrequency() { + return EMS_Sys_Status.emsPollFrequency; +} + bool ems_getTxCapable() { - if ((millis() - EMS_Sys_Status.emsPollTimestamp) > EMS_POLL_TIMEOUT) { + if ((EMS_Sys_Status.emsPollFrequency == 0) || (EMS_Sys_Status.emsPollFrequency > EMS_POLL_TIMEOUT)) { EMS_Sys_Status.emsTxCapable = false; } return EMS_Sys_Status.emsTxCapable; @@ -577,7 +581,6 @@ void _createValidate() { EMS_TxQueue.unshift(new_EMS_TxTelegram); // add back to queue making it first to be picked up next (FIFO) } - /* * Entry point triggered by an interrupt in emsuart.cpp * length is only data bytes, excluding the BRK @@ -588,9 +591,9 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { if ((length != 0) && (telegram[0] != 0x00)) { _ems_readTelegram(telegram, length); } - // now clear the Rx buffer just be safe and prevent duplicates - for (uint8_t i = 0; i < EMS_MAXBUFFERSIZE; telegram[i++] = 0x00) - ; + + // clear the Rx buffer just be safe and prevent duplicates + memset(telegram, 0, EMS_MAXBUFFERSIZE); } /** @@ -600,9 +603,10 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { void _ems_readTelegram(uint8_t * telegram, uint8_t length) { // create the Rx package static _EMS_RxTelegram EMS_RxTelegram; - EMS_RxTelegram.length = length; - EMS_RxTelegram.telegram = telegram; - EMS_RxTelegram.timestamp = millis(); + static uint32_t _last_emsPollFrequency = 0; + EMS_RxTelegram.length = length; + EMS_RxTelegram.telegram = telegram; + EMS_RxTelegram.timestamp = millis(); // check if we just received a single byte // it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) @@ -610,10 +614,12 @@ void _ems_readTelegram(uint8_t * telegram, uint8_t length) { if (length == 1) { uint8_t value = telegram[0]; // 1st byte of data package + EMS_Sys_Status.emsPollFrequency = (EMS_RxTelegram.timestamp - _last_emsPollFrequency); + _last_emsPollFrequency = EMS_RxTelegram.timestamp; + // check first for a Poll for us if (value == (EMS_ID_ME | 0x80)) { - EMS_Sys_Status.emsPollTimestamp = EMS_RxTelegram.timestamp; // store when we received a last poll - EMS_Sys_Status.emsTxCapable = true; + EMS_Sys_Status.emsTxCapable = true; // do we have something to send thats waiting in the Tx queue? // if so send it if the Queue is not in a wait state @@ -1396,8 +1402,8 @@ void ems_printTxQueue() { strlcpy(sType, "?", sizeof(sType)); } - char addedTime[15] = {0}; - unsigned long upt = EMS_TxTelegram.timestamp; + char addedTime[15] = {0}; + uint32_t upt = EMS_TxTelegram.timestamp; snprintf(addedTime, sizeof(addedTime), "(%02d:%02d:%02d)", diff --git a/src/ems.h b/src/ems.h index e1e97f33a..73551e1cc 100644 --- a/src/ems.h +++ b/src/ems.h @@ -89,8 +89,8 @@ typedef struct { _EMS_SYS_LOGGING emsLogging; // logging bool emsRefreshed; // fresh data, needs to be pushed out to MQTT bool emsBusConnected; // is there an active bus - unsigned long emsRxTimestamp; // timestamp of last EMS message received - unsigned long emsPollTimestamp; // timestamp of last EMS poll sent to us + uint32_t emsRxTimestamp; // timestamp of last EMS message received + uint32_t emsPollFrequency; // time between EMS polls bool emsTxCapable; // able to send via Tx bool emsTxDisabled; // true to prevent all Tx uint8_t txRetryCount; // # times the last Tx was re-sent @@ -109,7 +109,7 @@ typedef struct { uint8_t comparisonOffset; // offset of where the byte is we want to compare too later uint8_t comparisonPostRead; // after a successful write call this to read bool forceRefresh; // should we send to MQTT after a successful Tx? - unsigned long timestamp; // when created + uint32_t timestamp; // when created uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; } _EMS_TxTelegram; @@ -120,7 +120,6 @@ typedef struct { uint8_t length; // length in bytes } _EMS_RxTelegram; - // default empty Tx const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { EMS_TX_TELEGRAM_INIT, // action @@ -297,6 +296,7 @@ bool ems_getEmsRefreshed(); uint8_t ems_getThermostatModel(); void ems_discoverModels(); bool ems_getTxCapable(); +uint32_t ems_getPollFrequency(); void ems_scanDevices(); void ems_printAllTypes(); diff --git a/src/ems_devices.h b/src/ems_devices.h index e959895a0..e3cda412c 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -122,6 +122,7 @@ const _Boiler_Type Boiler_Types[] = { {EMS_MODEL_UBA, 123, 0x08, "Buderus GB172/Nefit Trendline"}, {EMS_MODEL_UBA, 115, 0x08, "Nefit Topline Compact"}, {EMS_MODEL_UBA, 203, 0x08, "Buderus Logamax U122"}, + {EMS_MODEL_UBA, 208, 0x08, "Buderus Logamax plus"}, {EMS_MODEL_UBA, 64, 0x08, "Sieger BK15 Boiler/Nefit Smartline"}, {EMS_MODEL_UBA, 95, 0x08, "Bosch Condens 2500"} diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 65b479b0b..4815d3d03 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -177,6 +177,7 @@ void ICACHE_FLASH_ATTR emsuart_tx_brk() { void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { for (uint8_t i = 0; i < len; i++) { USF(EMSUART_UART) = buf[i]; + //delayMicroseconds(EMS_TX_BRK_WAIT); } emsuart_tx_brk(); } diff --git a/src/my_config.h b/src/my_config.h index 8f55c891f..0cc8fb30a 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -22,6 +22,7 @@ #define MQTT_RETAIN false #define MQTT_KEEPALIVE 120 // 2 minutes #define MQTT_QOS 1 +#define MQTT_MAX_SIZE 700 // max size of a JSON object. See https://arduinojson.org/v6/assistant/ // MQTT for thermostat #define TOPIC_THERMOSTAT_DATA "thermostat_data" // for sending thermostat values to MQTT diff --git a/src/version.h b/src/version.h index 69498c119..11aef61d0 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.7.0b1" +#define APP_VERSION "1.6.1b1" #define APP_HOSTNAME "ems-esp"