diff --git a/.gitignore b/.gitignore index ebd9e5140..07cb21044 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ .gcc-flags.json .vscode .env +.DS_Store platformio.ini lib/readme.txt .travis.yml scripts/stackdmp.txt +*.bin diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2bd16c5..4e81e9f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ 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.8.1dev] 2019-07-27 + +### Added + +- Added back -DCRASH in Debug build target for capturing any ESP8266 stack dumps during crashes +- Web Interface, for checking stats and setting wifi credentials. See wiki for more details. +- reset firmware option. If the reset button on the ESP is pressed during boot up sequence (the LED is flashing very fast) all settings are erased and goes into AP mode. +- Added tx_mode back with options 0,1 and 2 until we've fixed option 2 that works for everyone and doesn't reset ESP +- More solar module data captured, thanks to @Vuego123 +- Detect thermostat mode for EMS+ RC300/Moduline 3000 +- MQTT message to set boiler flowtemp (`boiler_cmd_flowtemp`). See [wiki](https://github.com/proddy/EMS-ESP/wiki/MQTT). + +### Fixed + +- Detecting unset values in the SPIFFS and setting default values +- Bosch Easy Connect wrongly classified as a thermostat +- Correctly handle telegrams who's size are exactly 32 bytes (e.g. 0x19 MonitorSlow) +- Telnet also available when in AP mode +- Handling of thermostat temperatures that were single bytes and couldn't exceed 25.5 (0xFF) degrees! + +### Changed + +- Improved handling of Solar Modules (thanks @Vuego123) +- `publish_wait` renamed to `publish_time`, a value of 0 means disabling all MQTT sending +- How signed shorts are handled such as the current and setpoint temps on RC300s +- Stopped automatic refresh of web page, which causes crashes/memory loss after a short time +- Support HA 0.96 climate component changes +- -DDEFAULT_NO_SERIAL changed to -DFORCE_SERIAL +- some code cleanups, removing NULLS and moving some things fron heap to stack to prevent memory fragmentation + ## [1.8.0] 2019-06-15 ### Added @@ -376,4 +406,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] 2018-05-14 -- Initial development version \ No newline at end of file +- Initial development version diff --git a/doc/home_assistant/climate.yaml b/doc/home_assistant/climate.yaml index b344c66fb..f23edba77 100644 --- a/doc/home_assistant/climate.yaml +++ b/doc/home_assistant/climate.yaml @@ -21,8 +21,8 @@ - platform: mqtt name: boiler modes: - - "on" - - "off" + - "auto" + - "off" min_temp: 40 max_temp: 60 temp_step: 1 @@ -31,6 +31,7 @@ temperature_command_topic: "home/ems-esp/boiler_cmd_wwtemp" current_temperature_template: "{{ value_json.wWCurTmp }}" temperature_state_template: "{{ value_json.wWSelTemp }}" - mode_state_template: "{{ value_json.wWActivated }}" + mode_state_template: "{% if value_json.wWActivated == 'off' %} off {% else %} auto {% endif %}" mode_state_topic: "home/ems-esp/boiler_data" mode_command_topic: "home/ems-esp/wwactivated" + \ No newline at end of file diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp index 48d7d4422..6d0077954 100644 --- a/lib/MyESP/MyESP.cpp +++ b/lib/MyESP/MyESP.cpp @@ -1,20 +1,23 @@ /* * MyESP - my ESP helper class to handle WiFi, MQTT and Telnet * - * Paul Derbyshire - December 2018 + * Paul Derbyshire - first revision: December 2018 * * Ideas borrowed from Espurna https://github.com/xoseperez/espurna */ #include "MyESP.h" +#ifdef CRASH EEPROM_Rotate EEPROMr; +#endif union system_rtcmem_t { struct { - uint8_t stability_counter; - uint8_t reset_reason; - uint16_t _reserved_; + uint8_t stability_counter; + uint8_t reset_reason; + uint8_t boot_status; + uint8_t _reserved_; } parts; uint32_t value; }; @@ -39,10 +42,10 @@ MyESP::MyESP() { _fs_callback = NULL; _fs_settings_callback = NULL; - _helpProjectCmds = NULL; - _helpProjectCmds_count = 0; + _web_callback = NULL; + + _serial = false; - _use_serial = false; _heartbeat = false; _mqtt_host = NULL; _mqtt_password = NULL; @@ -59,6 +62,8 @@ MyESP::MyESP() { _mqtt_last_connection = 0; _mqtt_connecting = false; + _firstInstall = false; + _wifi_password = NULL; _wifi_ssid = NULL; _wifi_callback = NULL; @@ -66,6 +71,7 @@ MyESP::MyESP() { _ota_pre_callback = NULL; _ota_post_callback = NULL; + _ota_doing_update = false; _suspendOutput = false; @@ -135,7 +141,7 @@ void MyESP::myDebug_P(PGM_P format_P, ...) { // use Serial? bool MyESP::getUseSerial() { - return (_use_serial); + return (_serial); } // heartbeat @@ -144,7 +150,7 @@ bool MyESP::getHeartbeat() { } // init heap ram -uint32_t MyESP::getInitialFreeHeap() { +uint32_t MyESP::_getInitialFreeHeap() { static uint32_t _heap = 0; if (0 == _heap) { @@ -154,8 +160,10 @@ uint32_t MyESP::getInitialFreeHeap() { return _heap; } -uint32_t MyESP::getUsedHeap() { - return getInitialFreeHeap() - ESP.getFreeHeap(); +// used heap mem +// note calls to getFreeHeap sometimes causes some ESPs to crash +uint32_t MyESP::_getUsedHeap() { + return _getInitialFreeHeap() - ESP.getFreeHeap(); } // called when WiFi is connected, and used to start OTA, MQTT @@ -187,8 +195,8 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { _wifi_connected = true; // finally if we don't want Serial anymore, turn it off - if (!_use_serial) { - myDebug_P(PSTR("Disabling serial port communication.")); + if (!_serial) { + myDebug_P(PSTR("[SYSTEM] Disabling serial port communication.")); SerialAndTelnet.flush(); // flush so all buffer is printed to serial SerialAndTelnet.setSerial(NULL); } @@ -198,25 +206,24 @@ 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 successful } if (code == MESSAGE_ACCESSPOINT_CREATED) { _wifi_connected = true; - myDebug_P(PSTR("[WIFI] MODE AP --------------------------------------")); + myDebug_P(PSTR("[WIFI] MODE AP")); myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str()); 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 - if (!_use_serial) { - SerialAndTelnet.setSerial(&Serial); - _use_serial = true; + // finally if we don't want Serial anymore, turn it off + if (!_serial) { + myDebug_P(PSTR("[SYSTEM] Disabling serial port communication.")); + SerialAndTelnet.flush(); // flush so all buffer is printed to serial + SerialAndTelnet.setSerial(NULL); } - myDebug_P(PSTR("Enabling serial port output")); - // call any final custom settings if (_wifi_callback) { _wifi_callback(); @@ -237,6 +244,38 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { myDebug_P(PSTR("[WIFI] Disconnected")); _wifi_connected = false; } + + if (code == MESSAGE_SCANNING) { + myDebug_P(PSTR("[WIFI] Scanning")); + } + + if (code == MESSAGE_SCAN_FAILED) { + myDebug_P(PSTR("[WIFI] Scan failed")); + } + + if (code == MESSAGE_NO_NETWORKS) { + myDebug_P(PSTR("[WIFI] No networks found")); + } + + if (code == MESSAGE_NO_KNOWN_NETWORKS) { + myDebug_P(PSTR("[WIFI] No known networks found")); + } + + if (code == MESSAGE_FOUND_NETWORK) { + myDebug_P(PSTR("[WIFI] %s"), parameter); + } + + if (code == MESSAGE_CONNECT_WAITING) { + // too much noise + } + + if (code == MESSAGE_ACCESSPOINT_CREATING) { + myDebug_P(PSTR("[WIFI] Creating access point")); + } + + if (code == MESSAGE_ACCESSPOINT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not create access point")); + } } // return true if in WiFi AP mode @@ -266,7 +305,7 @@ void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { // Restart the device if (strcmp(topic, MQTT_TOPIC_RESTART) == 0) { myDebug_P(PSTR("[MQTT] Received restart command"), message); - myESP.resetESP(); + resetESP(); return; } @@ -274,7 +313,7 @@ void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { // for example with HA it sends the system time from the server if (strcmp(topic, MQTT_TOPIC_START) == 0) { myDebug_P(PSTR("[MQTT] Received boottime: %s"), message); - myESP.setBoottime(message); + setBoottime(message); return; } @@ -330,7 +369,7 @@ void MyESP::_mqttOnConnect() { // MQTT setup void MyESP::_mqtt_setup() { if (!_mqtt_host) { - myDebug_P(PSTR("[MQTT] disabled")); + myDebug_P(PSTR("[MQTT] is disabled")); } mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); }); @@ -395,6 +434,7 @@ void MyESP::setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_po void MyESP::_OTACallback() { myDebug_P(PSTR("[OTA] Start")); +#ifdef CRASH // If we are not specifically reserving the sectors we are using as // EEPROM in the memory layout then any OTA upgrade will overwrite // all but the last one. @@ -407,6 +447,12 @@ void MyESP::_OTACallback() { // See onError callback below. EEPROMr.rotate(false); EEPROMr.commit(); +#endif + + // stop the web server + webServer.close(); + + _ota_doing_update = true; if (_ota_pre_callback) { (_ota_pre_callback)(); // call custom function @@ -425,7 +471,8 @@ void MyESP::_ota_setup() { ArduinoOTA.onStart([this]() { _OTACallback(); }); ArduinoOTA.onEnd([this]() { myDebug_P(PSTR("[OTA] Done, restarting...")); - _deferredReset(100, CUSTOM_RESET_OTA); + _ota_doing_update = false; + _deferredReset(500, CUSTOM_RESET_OTA); }); ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) { @@ -449,8 +496,10 @@ void MyESP::_ota_setup() { else if (error == OTA_END_ERROR) myDebug_P(PSTR("[OTA] End Failed")); - // There's been an error, reenable rotation +#ifdef CRASH + // There's been an error, reenable eeprom rotation EEPROMr.rotate(true); +#endif }); } @@ -464,14 +513,14 @@ void MyESP::setBoottime(const char * boottime) { // eeprom void MyESP::_eeprom_setup() { +#ifdef CRASH EEPROMr.size(4); EEPROMr.begin(SPI_FLASH_SEC_SIZE); +#endif } // Set callback of sketch function to process project messages -void MyESP::setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { - _helpProjectCmds = cmds; // command list - _helpProjectCmds_count = count; // number of commands +void MyESP::setTelnet(telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { _telnetcommand_callback = callback_cmd; // external function to handle commands _telnet_callback = callback; } @@ -480,18 +529,14 @@ void MyESP::_telnetConnected() { myDebug_P(PSTR("[TELNET] Telnet connection established")); _consoleShowHelp(); // Show the initial message +#ifdef CRASH // show crash dump if just restarted after a fatal crash uint32_t crash_time; EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); if ((crash_time != 0) && (crash_time != 0xFFFFFFFF)) { - crashDump(); - /* - // clear crash data - crash_time = 0xFFFFFFFF; - EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); - EEPROMr.commit(); - */ + myDebug_P(PSTR("[SYSTEM] There is stack data available from the last system crash. Use 'crash dump' to view and 'crash clear' to reset")); } +#endif // call callback if (_telnet_callback) { @@ -539,29 +584,15 @@ void MyESP::_consoleShowHelp() { myDebug_P(PSTR("* Commands:")); myDebug_P(PSTR("* ?=help, CTRL-D/quit=exit telnet session")); myDebug_P(PSTR("* set, system, reboot")); - myDebug_P(PSTR("* crash ")); +#ifdef CRASH + myDebug_P(PSTR("* 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)); - } - } + // call callback function + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_SHOWCMD); } + myDebug_P(PSTR("")); // newline } @@ -570,31 +601,14 @@ void MyESP::_printSetCommands() { 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 [value]")); - myDebug_P(PSTR("* set [value]")); - myDebug_P(PSTR("* set serial ")); + myDebug_P(PSTR(" set erase")); + myDebug_P(PSTR(" set [value]")); + myDebug_P(PSTR(" set [value]")); + myDebug_P(PSTR(" set serial ")); - // 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("* 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)); - } - } + // call callback function + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_SHOWSET); } myDebug_P(PSTR("")); // newline @@ -622,7 +636,7 @@ void MyESP::_printSetCommands() { } myDebug_P(PSTR("")); // newline - myDebug_P(PSTR(" serial=%s"), (_use_serial) ? "on" : "off"); + myDebug_P(PSTR(" serial=%s"), (_serial) ? "on" : "off"); myDebug_P(PSTR(" heartbeat=%s"), (_heartbeat) ? "on" : "off"); // print any custom settings @@ -634,7 +648,7 @@ void MyESP::_printSetCommands() { // reset / restart void MyESP::resetESP() { myDebug_P(PSTR("* Reboot ESP...")); - _deferredReset(100, CUSTOM_RESET_TERMINAL); + _deferredReset(500, CUSTOM_RESET_TERMINAL); end(); #if defined(ARDUINO_ARCH_ESP32) ESP.restart(); @@ -711,16 +725,16 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) ok = true; } else if (strcmp(setting, "serial") == 0) { - ok = true; - _use_serial = false; + ok = true; + _serial = false; if (value) { if (strcmp(value, "on") == 0) { - _use_serial = true; - ok = true; + _serial = true; + ok = true; myDebug_P(PSTR("Reboot ESP to activate Serial mode.")); } else if (strcmp(value, "off") == 0) { - _use_serial = false; - ok = true; + _serial = false; + ok = true; myDebug_P(PSTR("Reboot ESP to deactivate Serial mode.")); } else { ok = false; @@ -767,16 +781,9 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) } // 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::setUseSerial(bool b) { + _serial = b; + SerialAndTelnet.setSerial(b ? &Serial : NULL); } void MyESP::_telnetCommand(char * commandLine) { @@ -842,22 +849,28 @@ void MyESP::_telnetCommand(char * commandLine) { return; } - +#ifdef CRASH // crash command - if ((strcmp(ptrToCommandName, "crash") == 0) && (wc == 2)) { + if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { 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(false); + crashTest(atoi(value)); } else { - myDebug_P(PSTR("Error. Usage: crash ")); + myDebug_P(PSTR("Error. Usage: crash ")); } return; // don't call custom command line callback } +#endif // call callback function - (_telnetcommand_callback)(wc, commandLine); + if (_telnetcommand_callback) { + (_telnetcommand_callback)(wc, commandLine); + } } // returns WiFi hostname as a String object @@ -913,12 +926,26 @@ unsigned long MyESP::_getUptime() { return uptime_seconds; } -// reason code +// init RTC mem void MyESP::_rtcmemInit() { memset((uint32_t *)RTCMEM_ADDR, 0, sizeof(uint32_t) * RTCMEM_BLOCKS); Rtcmem->magic = RTCMEM_MAGIC; } +uint8_t MyESP::getSystemBootStatus() { + system_rtcmem_t data; + data.value = Rtcmem->sys; + return data.parts.boot_status; +} + +void MyESP::_setSystemBootStatus(uint8_t status) { + system_rtcmem_t data; + data.value = Rtcmem->sys; + data.parts.boot_status = status; + Rtcmem->sys = data.value; + // myDebug("*** setting boot status to %d", data.parts.boot_status); +} + uint8_t MyESP::_getSystemStabilityCounter() { system_rtcmem_t data; data.value = Rtcmem->sys; @@ -945,6 +972,7 @@ void MyESP::_setSystemResetReason(uint8_t reason) { Rtcmem->sys = data.value; } +// system_get_rst_info() result is cached by the Core init for internal use uint32_t MyESP::getSystemResetReason() { return resetInfo.reason; } @@ -960,13 +988,26 @@ void MyESP::_setCustomResetReason(uint8_t reason) { _setSystemResetReason(reason); } +// returns false if not set and needs to be intialized, causing all rtcmem data to be wiped bool MyESP::_rtcmemStatus() { bool readable; - switch (getSystemResetReason()) { - case REASON_EXT_SYS_RST: - case REASON_WDT_RST: - case REASON_DEFAULT_RST: + uint32_t reason = getSystemResetReason(); + + // the last reset could have been caused by manually pressing the reset button + // so before wiping, capture the boot sequence + if (reason == REASON_EXT_SYS_RST) { // external system reset + if (getSystemBootStatus() == MYESP_BOOTSTATUS_BOOTING) { + _setSystemBootStatus(MYESP_BOOTSTATUS_RESETNEEDED); + } else { + _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON); + } + } + + switch (reason) { + //case REASON_EXT_SYS_RST: // external system reset + case REASON_WDT_RST: // hardware watch dog reset + case REASON_DEFAULT_RST: // normal startup by power on readable = false; break; default: @@ -978,12 +1019,12 @@ bool MyESP::_rtcmemStatus() { return readable; } -bool MyESP::rtcmemStatus() { +bool MyESP::_getRtcmemStatus() { return _rtcmem_status; } -unsigned char MyESP::_getCustomResetReason() { - static unsigned char status = 255; +uint8_t MyESP::_getCustomResetReason() { + static uint8_t status = 255; if (status == 255) { if (_rtcmemStatus()) status = _getSystemResetReason(); @@ -995,22 +1036,22 @@ unsigned char MyESP::_getCustomResetReason() { return status; } -void MyESP::_deferredReset(unsigned long delaytime, unsigned char reason) { - delay(delaytime); +void MyESP::_deferredReset(unsigned long delaytime, uint8_t reason) { + _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON); _setCustomResetReason(reason); + delay(delaytime); } -// Call this method on boot with start=true to increase the crash counter -// Call it again once the system is stable to decrease the counter +// Call this method on boot with stable=true to reset the crash counter +// Each call increments the counter // If the counter reaches SYSTEM_CHECK_MAX then the system is flagged as unstable void MyESP::_setSystemCheck(bool stable) { uint8_t value = 0; if (stable) { value = 0; // system is ok - // myDebug_P(PSTR("[SYSTEM] System OK\n")); } else { - if (!rtcmemStatus()) { + if (!_getRtcmemStatus()) { _setSystemStabilityCounter(1); return; } @@ -1020,17 +1061,27 @@ void MyESP::_setSystemCheck(bool stable) { if (++value > SYSTEM_CHECK_MAX) { _systemStable = false; value = 0; // system is unstable - myDebug_P(PSTR("[SYSTEM] Warning, system UNSTABLE\n")); + myDebug_P(PSTR("[SYSTEM] Warning, system UNSTABLE.")); + + /* + // enable Serial again + if (!_serial) { + SerialAndTelnet.setSerial(&Serial); + _serial = true; + } + */ } } _setSystemStabilityCounter(value); } -bool MyESP::getSystemCheck() { +// return if system is stable (false=bad) +bool MyESP::_getSystemCheck() { return _systemStable; } +// periodically check if system is stable void MyESP::_systemCheckLoop() { static bool checked = false; if (!checked && (millis() > SYSTEM_CHECK_TIME)) { @@ -1069,7 +1120,7 @@ void MyESP::showSystemStats() { myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage()); - if (!getSystemCheck()) { + if (!_getSystemCheck()) { myDebug_P(PSTR(" [SYSTEM] Device is in SAFE MODE")); } @@ -1083,6 +1134,13 @@ void MyESP::showSystemStats() { myDebug_P(PSTR(" [WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); + if (isMQTTConnected()) { + myDebug_P(PSTR(" [MQTT] is connected (with heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled"); + } else { + myDebug_P(PSTR(" [MQTT] is disconnected")); + } + +#ifdef CRASH char output_str[80] = {0}; char buffer[16] = {0}; myDebug_P(PSTR(" [EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); @@ -1094,6 +1152,7 @@ void MyESP::showSystemStats() { strlcat(output_str, " ", sizeof(output_str)); } myDebug(output_str); +#endif #ifdef ARDUINO_BOARD myDebug_P(PSTR(" [SYSTEM] Board: %s"), ARDUINO_BOARD); @@ -1107,7 +1166,6 @@ void MyESP::showSystemStats() { 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()); unsigned char reason = _getCustomResetReason(); if (reason > 0) { char buffer[32]; @@ -1119,7 +1177,7 @@ void MyESP::showSystemStats() { } myDebug_P(PSTR(" [SYSTEM] Restart count: %d"), _getSystemStabilityCounter()); - myDebug_P(PSTR(" [SYSTEM] rtcmem status:%u blocks:%u addr:0x%p"), _rtcmemStatus(), RtcmemSize, Rtcmem); + myDebug_P(PSTR(" [SYSTEM] rtcmem status: blocks:%u addr:0x%p"), RtcmemSize, Rtcmem); for (uint8_t block = 0; block < RtcmemSize; ++block) { myDebug_P(PSTR(" [SYSTEM] rtcmem %02u: %u"), block, reinterpret_cast(RTCMEM_ADDR)[block]); } @@ -1140,7 +1198,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); - uint32_t total_memory = getInitialFreeHeap(); + uint32_t total_memory = _getInitialFreeHeap(); uint32_t free_memory = ESP.getFreeHeap(); myDebug(" [MEM] Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)", @@ -1166,7 +1224,7 @@ void MyESP::_heartbeatCheck(bool force = false) { return; } - uint32_t total_memory = getInitialFreeHeap(); + uint32_t total_memory = _getInitialFreeHeap(); uint32_t free_memory = ESP.getFreeHeap(); uint8_t mem_available = 100 * free_memory / total_memory; // as a % @@ -1213,7 +1271,7 @@ void MyESP::_telnetHandle() { if (charsRead > 0) { charsRead = 0; // is static, so have to reset _suspendOutput = false; - if (_use_serial) { + if (_serial) { SerialAndTelnet.serialPrint('\n'); // force newline if in Serial } _telnetCommand(_command); @@ -1302,14 +1360,14 @@ void MyESP::_mqttConnect() { // Setup everything we need void MyESP::setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback) { // Check SSID too long or missing - if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > 31) { + if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > MAX_SSID_LEN) { _wifi_ssid = NULL; } else { _wifi_ssid = strdup(wifi_ssid); } // Check PASS too long - if (!wifi_password || *wifi_ssid == 0x00 || strlen(wifi_password) > 31) { + if (!wifi_password || *wifi_ssid == 0x00 || strlen(wifi_password) > MAX_PWD_LEN) { _wifi_password = NULL; } else { _wifi_password = strdup(wifi_password); @@ -1425,15 +1483,21 @@ void MyESP::_fs_printConfig() { // format File System void MyESP::_fs_eraseConfig() { - myDebug_P(PSTR("[FS] Erasing settings, please wait a few seconds. ESP will " + myDebug_P(PSTR("[FS] Erasing all settings, please wait a few seconds. ESP will " "automatically restart when finished.")); - if (SPIFFS.format()) { + if (SPIFFS.remove(MYEMS_CONFIG_FILE)) { delay(1000); // wait 1 second - resetESP(); + SerialAndTelnet.flush(); + resetESP(); // hard reset } } +// custom callback for web info +void MyESP::setWeb(web_callback_f callback_web) { + _web_callback = callback_web; +} + void MyESP::setSettings(fs_callback_f callback_fs, fs_settings_callback_f callback_settings_fs) { _fs_callback = callback_fs; _fs_settings_callback = callback_settings_fs; @@ -1448,9 +1512,6 @@ bool MyESP::_fs_loadConfig() { myDebug_P(PSTR("[FS] Config file size is too large")); return false; } else if (size == 0) { - myDebug_P(PSTR("[FS] Failed to open config file")); - // file does not exist, so assume its the first install. Set serial to on - _use_serial = true; return false; } @@ -1482,9 +1543,14 @@ bool MyESP::_fs_loadConfig() { value = json["mqtt_password"]; _mqtt_password = (value) ? strdup(value) : NULL; - _use_serial = (bool)json["use_serial"]; + _heartbeat = (bool)json["heartbeat"]; // defaults to off - _heartbeat = (bool)json["heartbeat"]; +// serial is only on when booting +#ifdef FORCE_SERIAL + _serial = true; +#else + _serial = json["serial"]; +#endif // callback for loading custom settings // ok is false if there's a problem loading a custom setting (e.g. does not exist) @@ -1513,7 +1579,7 @@ bool MyESP::fs_saveConfig() { json["mqtt_host"] = _mqtt_host; json["mqtt_username"] = _mqtt_username; json["mqtt_password"] = _mqtt_password; - json["use_serial"] = _use_serial; + json["serial"] = _serial; json["heartbeat"] = _heartbeat; // callback for saving custom settings @@ -1531,7 +1597,6 @@ bool MyESP::fs_saveConfig() { return false; } - // Serialize JSON to file if (serializeJson(json, configFile) == 0) { myDebug_P(PSTR("[FS] Failed to write config file")); @@ -1557,12 +1622,27 @@ void MyESP::_fs_setup() { return; } + // if its flagged as a first install, re-create the initial config file and quit function + if (_firstInstall) { + myDebug_P(PSTR("[FS] Re-creating config file for initial install")); + fs_saveConfig(); + return; + } + // 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(); + _firstInstall = true; // flag as a first install } + // assume if the wifi ssid is empty, its a fresh install too + if ((_wifi_ssid == NULL)) { + _firstInstall = true; // flag as a first install + } + + myDebug_P(PSTR("[FS] Settings loaded from SPIFFS")); + // _fs_printConfig(); // enable for debugging } @@ -1596,14 +1676,6 @@ bool MyESP::isMQTTConnected() { } // return true if wifi is connected -// WL_NO_SHIELD = 255, // for compatibility with WiFi Shield library -// WL_IDLE_STATUS = 0, -// WL_NO_SSID_AVAIL = 1, -// WL_SCAN_COMPLETED = 2, -// WL_CONNECTED = 3, -// WL_CONNECT_FAILED = 4, -// WL_CONNECTION_LOST = 5, -// WL_DISCONNECTED = 6 bool MyESP::isWifiConnected() { return (_wifi_connected); } @@ -1630,6 +1702,7 @@ int MyESP::getWifiQuality() { return 2 * (dBm + 100); } +#ifdef CRASH /** * Save crash information in EEPROM * This function is called automatically if ESP8266 suffers an exception @@ -1779,41 +1852,320 @@ void MyESP::crashDump() { myDebug_P(PSTR("\nTo clean this dump use the command: %scrash clear%s\n"), COLOR_BOLD_ON, COLOR_BOLD_OFF); } +/* + * Force some crashes to test if stack collection and debugging works + */ +void MyESP::crashTest(uint8_t t) { + if (t == 1) { + myDebug_P(PSTR("[CRASH] Attempting to divide by zero ...")); + int result, zero; + zero = 0; + result = 1 / zero; + Serial.printf("Result = %d", result); + } + + if (t == 2) { + myDebug_P(PSTR("[CRASH] Attempting to read through a pointer to no object ...")); + int * nullPointer; + nullPointer = NULL; + // null pointer dereference - read + // attempt to read a value through a null pointer + Serial.println(*nullPointer); + } + + if (t == 3) { + myDebug_P(PSTR("[CRASH] Crashing with hardware WDT (%ld ms) ...\n"), millis()); + ESP.wdtDisable(); + while (true) { + // stay in an infinite loop doing nothing + // this way other process can not be executed + // + // Note: + // Hardware wdt kicks in if software wdt is unable to perfrom + // Nothing will be saved in EEPROM for the hardware wdt + } + } + + if (t == 4) { + myDebug_P(PSTR("[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 + } + } +} + +#else +void MyESP::crashTest(uint8_t t) { +} +void MyESP::crashClear() { +} +void MyESP::crashDump() { +} +void MyESP::crashInfo() { +} +#endif + +// default home web page +void MyESP::_webRootPage() { + char s[1000] = {0}; + + strlcpy(s, webCommonPage_start, sizeof(s)); + // strlcat(s, webCommonPage_start_refresh, sizeof(s)); + strlcat(s, webCommonPage_start_body, sizeof(s)); + + strlcat(s, "

", sizeof(s)); + strlcat(s, _app_name, sizeof(s)); + strlcat(s, " version ", sizeof(s)); + strlcat(s, _app_version, sizeof(s)); + strlcat(s, "

", sizeof(s)); + + strlcat(s, "

System stats:
", sizeof(s)); + + if (isAPmode()) { + strlcat(s, " Device is in Wifi Access Point mode with SSID ", sizeof(s)); + strlcat(s, jw.getAPSSID().c_str(), sizeof(s)); + strlcat(s, "", sizeof(s)); + } else { + char buf[4]; + strlcat(s, " Connected to wireless network ", sizeof(s)); + strlcat(s, _getESPhostname().c_str(), sizeof(s)); + strlcat(s, " with signal strength ", sizeof(s)); + strlcat(s, itoa(getWifiQuality(), buf, 10), sizeof(s)); + strlcat(s, "%", sizeof(s)); + } + + strlcat(s, isMQTTConnected() ? "
MQTT is connected\n" : " MQTT is disconnected\n", sizeof(s)); + strlcat(s, "
", sizeof(s)); + + // uptime + char buffer[200]; + uint32_t t = _getUptime(); // seconds + uint32_t d = t / 86400L; + uint32_t h = ((t % 86400L) / 3600L) % 60; + uint32_t rem = t % 3600L; + uint8_t m = rem / 60; + uint8_t sec = rem % 60; + sprintf(buffer, " System uptime: %d days %d hours %d minutes %d seconds", d, h, m, sec); + strlcat(s, buffer, sizeof(s)); + + // memory + //uint32_t total_memory = _getInitialFreeHeap(); + //uint32_t free_memory = ESP.getFreeHeap(); + //sprintf(buffer, " Memory: %d bytes free (%2u%%)
", free_memory, 100 * free_memory / total_memory); + //strlcat(s, buffer, sizeof(s)); + + strlcat(s, "

", sizeof(s)); + if (_web_callback) { + char custom[MYESP_MAXCHARBUFFER]; + (_web_callback)(custom); + strlcat(s, custom, sizeof(s)); + } + strlcat(s, "


", sizeof(s)); + + // check why we're here + if ((_firstInstall) || (_wifi_ssid == NULL)) { + strlcat(s, "

Looks like a first install! Go here to connect the System to your network.

", sizeof(s)); + } else { + strlcat(s, "

Go here to connect the System to your wireless network.

", sizeof(s)); + } + + strlcat(s, webCommonPage_end, sizeof(s)); + webServer.sendHeader("Content-Length", String(strlen(s))); + webServer.send(200, "text/html", s); +} + +// Creates a webpage that allows the user to change the SSID and Password from the browser +void MyESP::_webResetPage() { + char s[1000] = {0}; + + strlcpy(s, webCommonPage_start, sizeof(s)); + strlcat(s, webCommonPage_start_body, sizeof(s)); + + strlcat(s, "

", sizeof(s)); + strlcat(s, _app_name, sizeof(s)); + strlcat(s, " version ", sizeof(s)); + strlcat(s, _app_version, sizeof(s)); + strlcat(s, "

", sizeof(s)); + + // Check to see if we've been sent any arguments and instantly return if not + if (webServer.args() == 0) { + strlcat(s, "

", sizeof(s)); + + if (_wifi_ssid != NULL) { + strlcat(s, "Current wifi SSID is ", sizeof(s)); + strlcat(s, _wifi_ssid, sizeof(s)); + strlcat(s, ".
", sizeof(s)); + } + + strlcat(s, "
Please enter your new wifi credentials below.

", sizeof(s)); + + strlcat(s, webResetPage_form, sizeof(s)); + strlcat(s, webCommonPage_end, sizeof(s)); + webServer.sendHeader("Content-Length", String(strlen(s))); + webServer.send(200, "text/html", s); + + } else { + // Create a string containing all the arguments + // Check to see if there are new values (also doubles to check the length of the new value is long enough) + if (webServer.arg("newssid").length() <= MAX_SSID_LEN) { + if (webServer.arg("newssid").length() == 0) { + _wifi_ssid = NULL; + } else { + _wifi_ssid = strdup(webServer.arg("newssid").c_str()); + } + } + + if (webServer.arg("newpassword").length() <= MAX_PWD_LEN) { + if (webServer.arg("newpassword").length() == 0) { + _wifi_password = NULL; + } else { + _wifi_password = strdup(webServer.arg("newpassword").c_str()); + } + } + + // Store the new settings + fs_saveConfig(); + + // Reply with a web page to indicate success or failure + strlcat(s, webResetPage_post, sizeof(s)); + strlcat(s, webCommonPage_end, sizeof(s)); + webServer.sendHeader("Content-Length", String(strlen(s))); + webServer.send(200, "text/html", s); + + delay(500); + resetESP(); + } +} + +// reset all settings +void MyESP::_webResetAllPage() { + char s[1000] = {0}; + + strlcpy(s, webCommonPage_start, sizeof(s)); + strlcat(s, webCommonPage_start_body, sizeof(s)); + + strlcat(s, "

", sizeof(s)); + strlcat(s, _app_name, sizeof(s)); + strlcat(s, " version ", sizeof(s)); + strlcat(s, _app_version, sizeof(s)); + strlcat(s, "

", sizeof(s)); + + // Check to see if we've been sent any arguments and instantly return if not + if (webServer.args() == 0) { + strlcat(s, + "

Are you absolutely sure you want to erase all settings?
Typing 'yes' will restart the System and you'll need to reconnect to the wifi " + "Access Point called ems-esp.

", + sizeof(s)); + + strlcat(s, webResetAllPage_form, sizeof(s)); + strlcat(s, webCommonPage_end, sizeof(s)); + webServer.sendHeader("Content-Length", String(strlen(s))); + webServer.send(200, "text/html", s); + } else { + // delete all settings + if (webServer.arg("confirm") == "yes") { + _fs_eraseConfig(); + delay(1000); // wait 1 sec + resetESP(); + } + } +} + +// set up web server +void MyESP::_webserver_setup() { + webServer.on("/", [this]() { _webRootPage(); }); + webServer.on("/reset", [this]() { _webResetPage(); }); + webServer.on("/resetall", [this]() { _webResetAllPage(); }); + + webServer.begin(); + + myDebug_P(PSTR("[WEB] Web server started")); +} + +// bootup sequence +// quickly flash LED until we get a Wifi connection, or AP established +// 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 MyESP::_bootupSequence() { + uint8_t boot_status = getSystemBootStatus(); + + if ((boot_status == MYESP_BOOTSTATUS_BOOTED) || (millis() <= MYESP_BOOTUP_DELAY)) { + return; // already booted, or still starting up + } + + // only kick in after a few seconds + if (boot_status == MYESP_BOOTSTATUS_POWERON) { + _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTING); + } + + static uint32_t last_bootupflash = 0; + + // flash LED quickly + if ((millis() - last_bootupflash > MYESP_BOOTUP_FLASHDELAY)) { + last_bootupflash = millis(); + int state = digitalRead(LED_BUILTIN); + digitalWrite(LED_BUILTIN, !state); + } + + if (isWifiConnected()) { + _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTED); // completed, reset flag + digitalWrite(LED_BUILTIN, LOW); // turn off LED + } +} + // setup MyESP void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version) { _app_hostname = strdup(app_hostname); _app_name = strdup(app_name); _app_version = strdup(app_version); - getInitialFreeHeap(); // get initial free mem - - _rtcmemSetup(); _telnet_setup(); // Telnet setup, called first to set Serial - _eeprom_setup(); // set up EEPROM for storing crash data, if compiled with -DCRASH - _fs_setup(); // SPIFFS setup, do this first to get values - _wifi_setup(); // WIFI setup - _ota_setup(); // init OTA // print a welcome message - myDebug_P(PSTR("\n* %s version %s"), _app_name, _app_version); - SerialAndTelnet.flush(); + myDebug_P(PSTR("\n\n* %s version %s"), _app_name, _app_version); + + // set up onboard LED + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, HIGH); + + _getInitialFreeHeap(); // get initial free mem + _rtcmemSetup(); // rtc internal mem setup + + if (getSystemBootStatus() == MYESP_BOOTSTATUS_RESETNEEDED) { + myDebug_P(PSTR("** resetting all settings")); + _firstInstall = true; // flag as an initial install so the config file will be recreated + } + + _eeprom_setup(); // set up EEPROM for storing crash data, if compiled with -DCRASH + _fs_setup(); // SPIFFS setup, do this first to get values + _wifi_setup(); // WIFI setup + _ota_setup(); // init OTA + _webserver_setup(); // init web server _setSystemCheck(false); // reset system check - _heartbeatCheck(true); // force heartbeat + _heartbeatCheck(true); // force heartbeat check (not the MQTT one) + + SerialAndTelnet.flush(); } /* * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc */ void MyESP::loop() { + jw.loop(); // WiFi + ArduinoOTA.handle(); // OTA + + if (_ota_doing_update) { + return; // quit if in the middle of an OTA update + } + _calculateLoad(); _systemCheckLoop(); _heartbeatCheck(); - + _bootupSequence(); + webServer.handleClient(); _telnetHandle(); - jw.loop(); // WiFi - ArduinoOTA.handle(); // OTA - _mqttConnect(); // MQTT + _mqttConnect(); yield(); // ...and breath } diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index 733d1e0c5..74dcc2a4a 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -6,23 +6,26 @@ #pragma once -#ifndef MyEMS_h -#define MyEMS_h +#ifndef MyESP_h +#define MyESP_h -#define MYESP_VERSION "1.1.16" +#define MYESP_VERSION "1.1.24" #include #include #include // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127 -#include +#include #include #include // https://github.com/xoseperez/justwifi #include // modified from https://github.com/yasheena/telnetspy +#ifdef CRASH #include +#endif + extern "C" { +void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); #include "user_interface.h" -void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); extern struct rst_info resetInfo; } @@ -38,10 +41,10 @@ extern struct rst_info resetInfo; #define MYEMS_CONFIG_FILE "/config.json" -#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) +#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) = 30 seconds // WIFI -#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms +#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms (10 seconds) #define WIFI_RECONNECT_INTERVAL 600000 // If could not connect to WIFI, retry after this time in ms. 10 minutes // MQTT @@ -65,6 +68,8 @@ extern struct rst_info resetInfo; #define TELNET_MAX_COMMAND_LENGTH 80 // length of a command #define TELNET_EVENT_CONNECT 1 #define TELNET_EVENT_DISCONNECT 0 +#define TELNET_EVENT_SHOWCMD 10 +#define TELNET_EVENT_SHOWSET 20 // ANSI Colors #define COLOR_RESET "\x1B[0m" @@ -143,12 +148,11 @@ PROGMEM const char * const custom_reset_string[] = {custom_reset_hardware, cus #define RTCMEM_OFFSET 32u #define RTCMEM_ADDR (RTCMEM_ADDR_BASE + (RTCMEM_OFFSET * 4u)) #define RTCMEM_BLOCKS 96u -#define RTCMEM_MAGIC 0x45535075 +#define RTCMEM_MAGIC 0x45535076 struct RtcmemData { - uint32_t magic; // RTCMEM_MAGIC - uint32_t sys; // system reset reason (1-4) - uint32_t energy; // store energy count + uint32_t magic; // RTCMEM_MAGIC + uint32_t sys; // system details }; static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big"); @@ -165,6 +169,13 @@ typedef struct { typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION; +typedef enum { + MYESP_BOOTSTATUS_POWERON = 0, + MYESP_BOOTSTATUS_BOOTED = 1, + MYESP_BOOTSTATUS_BOOTING = 2, + MYESP_BOOTSTATUS_RESETNEEDED = 3 +} MYESP_BOOTSTATUS; // boot messages + typedef std::function mqtt_callback_f; typedef std::function wifi_callback_f; typedef std::function ota_callback_f; @@ -172,6 +183,7 @@ typedef std::function typedef std::function telnet_callback_f; typedef std::function fs_callback_f; typedef std::function fs_settings_callback_f; +typedef std::function web_callback_f; // calculates size of an 2d array at compile time template @@ -179,14 +191,55 @@ constexpr size_t ArraySize(T (&)[N]) { return N; } +template +void PROGMEM_readAnything(const T * sce, T & dest) { + memcpy_P(&dest, sce, sizeof(T)); +} + #define UPTIME_OVERFLOW 4294967295 // Uptime overflow value +// web min and max length of wifi ssid and password +#define MAX_SSID_LEN 32 +#define MAX_PWD_LEN 64 + +#define MYESP_BOOTUP_FLASHDELAY 50 // flash duration for LED at bootup sequence +#define MYESP_BOOTUP_DELAY 2000 // time before we open the window to reset. This is to stop resetting values when uploading firmware via USB + +// max size of char buffer for storing web page +#define MYESP_MAXCHARBUFFER 800 + +// Holds the admin webpage in the program memory +const char webCommonPage_start[] = "" + "" + ""; + +const char webCommonPage_start_body[] = ""; + +const char webCommonPage_end[] = ""; + +const char webResetPage_form[] = "
" + "" + "" + "" + "
"; + +const char webResetPage_post[] = + "

New wifi credentials set. System is now rebooting. Please wait a few seconds and then reconnect via telnet or browser to its new IP given address.

"; + +const char webResetAllPage_form[] = "
" + "" + "" + "
"; + // class definition class MyESP { public: MyESP(); ~MyESP(); + ESP8266WebServer webServer; // Web server on port 80 + // wifi void setWIFICallback(void (*callback)()); void setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); @@ -216,7 +269,7 @@ class MyESP { // debug & telnet void myDebug(const char * format, ...); void myDebug_P(PGM_P format_P, ...); - void setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback); + void setTelnet(telnetcommand_callback_f callback_cmd, telnet_callback_f callback); bool getUseSerial(); void setUseSerial(bool toggle); @@ -224,6 +277,9 @@ class MyESP { void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); bool fs_saveConfig(); + // Web + void setWeb(web_callback_f callback_web); + // Crash void crashClear(); void crashDump(); @@ -231,18 +287,17 @@ class MyESP { void crashInfo(); // general - void end(); - void loop(); - void begin(const char * app_hostname, const char * app_name, const char * app_version); - void setBoottime(const char * boottime); - void resetESP(); - int getWifiQuality(); - void showSystemStats(); - bool getHeartbeat(); - - // rtcmem and reset reason - bool rtcmemStatus(); + void end(); + void loop(); + void begin(const char * app_hostname, const char * app_name, const char * app_version); + void setBoottime(const char * boottime); + void resetESP(); + int getWifiQuality(); + void showSystemStats(); + bool getHeartbeat(); + uint32_t getSystemLoadAverage(); uint32_t getSystemResetReason(); + uint8_t getSystemBootStatus(); private: // mqtt @@ -271,7 +326,6 @@ class MyESP { bool _rtcmem_status; // wifi - DNSServer dnsServer; // For Access Point (AP) support void _wifiCallback(justwifi_messages_t code, char * parameter); void _wifi_setup(); wifi_callback_f _wifi_callback; @@ -285,6 +339,7 @@ class MyESP { ota_callback_f _ota_post_callback; void _ota_setup(); void _OTACallback(); + bool _ota_doing_update; // crash void _eeprom_setup(); @@ -298,8 +353,6 @@ class MyESP { 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 - uint8_t _helpProjectCmds_count; // # available commands void _consoleShowHelp(); telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands telnet_callback_f _telnet_callback; // callback for connect/disconnect @@ -316,19 +369,25 @@ class MyESP { fs_settings_callback_f _fs_settings_callback; void _printSetCommands(); + // web + web_callback_f _web_callback; + // general char * _app_hostname; char * _app_name; char * _app_version; char * _boottime; bool _suspendOutput; - bool _use_serial; + bool _serial; bool _heartbeat; unsigned long _getUptime(); String _buildTime(); + bool _firstInstall; // reset reason and rtcmem bool _rtcmemStatus(); + bool _getRtcmemStatus(); + void _rtcmemInit(); void _rtcmemSetup(); @@ -337,27 +396,33 @@ class MyESP { uint8_t _getSystemStabilityCounter(); void _setSystemStabilityCounter(uint8_t counter); - uint8_t _getSystemResetReason(); void _setSystemResetReason(uint8_t reason); + uint8_t _getCustomResetReason(); + void _setCustomResetReason(uint8_t reason); + uint8_t _getSystemResetReason(); - unsigned char _getCustomResetReason(); - void _setCustomResetReason(unsigned char reason); + void _setSystemBootStatus(uint8_t status); bool _systemStable; - - bool getSystemCheck(); + void _bootupSequence(); + bool _getSystemCheck(); void _systemCheckLoop(); void _setSystemCheck(bool stable); // load average (0..100) and heap ram - uint32_t getSystemLoadAverage(); void _calculateLoad(); uint32_t _load_average; - uint32_t getInitialFreeHeap(); - uint32_t getUsedHeap(); + uint32_t _getInitialFreeHeap(); + uint32_t _getUsedHeap(); // heartbeat void _heartbeatCheck(bool force); + + // webserver + void _webserver_setup(); + void _webRootPage(); + void _webResetPage(); + void _webResetAllPage(); }; extern MyESP myESP; diff --git a/lib/TelnetSpy/TelnetSpy.cpp b/lib/TelnetSpy/TelnetSpy.cpp index 6acbf9c4a..85716a0d8 100644 --- a/lib/TelnetSpy/TelnetSpy.cpp +++ b/lib/TelnetSpy/TelnetSpy.cpp @@ -573,6 +573,7 @@ void TelnetSpy::handle() { return; } if (!listening) { + if ((WiFi.status() == WL_DISCONNECTED) && (WiFi.getMode() & WIFI_AP)) { if (usedSer) { usedSer->println("[TELNET] in AP mode"); // added by Proddy diff --git a/platformio.ini-example b/platformio.ini-example index 28cff7858..fec449cab 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -1,6 +1,5 @@ ; ; PlatformIO Project Configuration File for EMS-ESP -; Uses PlatformIO 4.0 ; [platformio] @@ -8,24 +7,15 @@ default_envs = release ;default_envs = debug [common] -debug_flags = -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DTESTS -general_flags = -g -w -DNO_GLOBAL_EEPROM - -arduino_core_2_3_0 = espressif8266@1.5.0 -arduino_core_2_4_0 = espressif8266@1.6.0 -arduino_core_2_4_1 = espressif8266@1.7.3 -arduino_core_2_4_2 = espressif8266@1.8.0 -arduino_core_2_5_0 = espressif8266@2.0.4 -arduino_core_2_5_1 = espressif8266@2.1.1 -arduino_core_2_5_2 = espressif8266@2.2.1 -arduino_core_latest = espressif8266 +; -DMYESP_TIMESTAMP -DTESTS -DCRASH -DFORCE_SERIAL -DNO_GLOBAL_EEPROM -DLOGICANALYZER +extra_flags = -DNO_GLOBAL_EEPROM [env] board = d1_mini ; board = nodemcuv2 ; board = d1_mini_pro framework = arduino -platform = ${common.arduino_core_latest} +platform = espressif8266 lib_deps = CRC32 CircularBuffer @@ -41,16 +31,17 @@ monitor_speed = 115200 ;upload_port = ems-esp.local [env:debug] -build_flags = ${common.general_flags} ${common.debug_flags} +build_type = debug +build_flags = ${common.extra_flags} -DCRASH extra_scripts = pre:scripts/rename_fw.py [env:clean] extra_scripts = pre:scripts/clean_fw.py [env:release] -build_flags = ${common.general_flags} +build_flags = ${common.extra_flags} extra_scripts = pre:scripts/rename_fw.py [env:checkcode] -build_flags = ${common.general_flags} +build_flags = ${common.extra_flags} extra_scripts = scripts/checkcode.py diff --git a/scripts/analyze_stackdmp.py b/scripts/analyze_stackdmp.py index b5d156d03..1e29b2449 100755 --- a/scripts/analyze_stackdmp.py +++ b/scripts/analyze_stackdmp.py @@ -17,4 +17,8 @@ import os # 3fffffb0: feefeffe feefeffe 3ffe8558 40100b01 # <<[0-9]*)\\):$") +COUNTER_REGEX = re.compile('^epc1=(?P0x[0-9a-f]+) epc2=(?P0x[0-9a-f]+) epc3=(?P0x[0-9a-f]+) ' + 'excvaddr=(?P0x[0-9a-f]+) depc=(?P0x[0-9a-f]+)$') +CTX_REGEX = re.compile("^ctx: (?P.+)$") +POINTER_REGEX = re.compile('^sp: (?P[0-9a-f]+) end: (?P[0-9a-f]+) offset: (?P[0-9a-f]+)$') +STACK_BEGIN = '>>>stack>>>' +STACK_END = '<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$') + +StackLine = namedtuple("StackLine", ["offset", "content"]) + + +class ExceptionDataParser(object): + def __init__(self): + self.exception = None + + self.epc1 = None + self.epc2 = None + self.epc3 = None + self.excvaddr = None + self.depc = None + + self.ctx = None + + self.sp = None + self.end = None + self.offset = None + + self.stack = [] + + def _parse_exception(self, line): + match = EXCEPTION_REGEX.match(line) + if match is not None: + self.exception = int(match.group('exc')) + return self._parse_counters + return self._parse_exception + + def _parse_counters(self, line): + match = COUNTER_REGEX.match(line) + if match is not None: + self.epc1 = match.group("epc1") + self.epc2 = match.group("epc2") + self.epc3 = match.group("epc3") + self.excvaddr = match.group("excvaddr") + self.depc = match.group("depc") + return self._parse_ctx + return self._parse_counters + + def _parse_ctx(self, line): + match = CTX_REGEX.match(line) + if match is not None: + self.ctx = match.group("ctx") + return self._parse_pointers + return self._parse_ctx + + def _parse_pointers(self, line): + match = POINTER_REGEX.match(line) + if match is not None: + self.sp = match.group("sp") + self.end = match.group("end") + self.offset = match.group("offset") + return self._parse_stack_begin + return self._parse_pointers + + def _parse_stack_begin(self, line): + if line == STACK_BEGIN: + return self._parse_stack_line + return self._parse_stack_begin + + def _parse_stack_line(self, line): + if line != STACK_END: + match = STACK_REGEX.match(line) + if match is not None: + self.stack.append(StackLine(offset=match.group("off"), + content=(match.group("c1"), match.group("c2"), match.group("c3"), + match.group("c4")))) + return self._parse_stack_line + return None + + def parse_file(self, file, stack_only=False): + func = self._parse_exception + if stack_only: + func = self._parse_stack_begin + + for line in file: + func = func(line.strip()) + if func is None: + break + + if func is not None: + print("ERROR: Parser not complete!") + sys.exit(1) + + +class AddressResolver(object): + def __init__(self, tool_path, elf_path): + self._tool = tool_path + self._elf = elf_path + self._address_map = {} + + def _lookup(self, addresses): + cmd = [self._tool, "-aipfC", "-e", self._elf] + [addr for addr in addresses if addr is not None] + + if sys.version_info[0] < 3: + output = subprocess.check_output(cmd) + else: + output = subprocess.check_output(cmd, encoding="utf-8") + + line_regex = re.compile("^(?P[0-9a-fx]+): (?P.+)$") + + last = None + for line in output.splitlines(): + line = line.strip() + match = line_regex.match(line) + + if match is None: + if last is not None and line.startswith('(inlined by)'): + line = line [12:].strip() + self._address_map[last] += ("\n \-> inlined by: " + line) + continue + + if match.group("result") == '?? ??:0': + continue + + self._address_map[match.group("addr")] = match.group("result") + last = match.group("addr") + + def fill(self, parser): + addresses = [parser.epc1, parser.epc2, parser.epc3, parser.excvaddr, parser.sp, parser.end, parser.offset] + for line in parser.stack: + addresses.extend(line.content) + + self._lookup(addresses) + + def _sanitize_addr(self, addr): + if addr.startswith("0x"): + addr = addr[2:] + + fill = "0" * (8 - len(addr)) + return "0x" + fill + addr + + def resolve_addr(self, addr): + out = self._sanitize_addr(addr) + + if out in self._address_map: + out += ": " + self._address_map[out] + + return out + + def resolve_stack_addr(self, addr, full=True): + addr = self._sanitize_addr(addr) + if addr in self._address_map: + return addr + ": " + self._address_map[addr] + + if full: + return "[DATA (0x" + addr + ")]" + + return None + + +def print_addr(name, value, resolver): + print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value))) + + +def print_stack_full(lines, resolver): + print("stack:") + for line in lines: + print(line.offset + ":") + for content in line.content: + print(" " + resolver.resolve_stack_addr(content)) + + +def print_stack(lines, resolver): + print("stack:") + for line in lines: + for content in line.content: + out = resolver.resolve_stack_addr(content, full=False) + if out is None: + continue + print(out) + + +def print_result(parser, resolver, full=True, stack_only=False): + if not stack_only: + print('Exception: {} ({})'.format(parser.exception, EXCEPTIONS[parser.exception])) + + print("") + print_addr("epc1", parser.epc1, resolver) + print_addr("epc2", parser.epc2, resolver) + print_addr("epc3", parser.epc3, resolver) + print_addr("excvaddr", parser.excvaddr, resolver) + print_addr("depc", parser.depc, resolver) + + print("") + print("ctx: " + parser.ctx) + + print("") + print_addr("sp", parser.sp, resolver) + print_addr("end", parser.end, resolver) + print_addr("offset", parser.offset, resolver) + + print("") + if full: + print_stack_full(parser.stack, resolver) + else: + print_stack(parser.stack, resolver) + + +def parse_args(): + parser = argparse.ArgumentParser(description="decode ESP Stacktraces.") + + parser.add_argument("-p", "--platform", help="The platform to decode from", choices=PLATFORMS.keys(), + default="ESP8266") + parser.add_argument("-t", "--tool", help="Path to the xtensa toolchain", + default="~/.platformio/packages/toolchain-xtensa/") + parser.add_argument("-e", "--elf", help="path to elf file", required=True) + parser.add_argument("-f", "--full", help="Print full stack dump", action="store_true") + parser.add_argument("-s", "--stack_only", help="Decode only a stractrace", action="store_true") + parser.add_argument("file", help="The file to read the exception data from ('-' for STDIN)", default="-") + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + if args.file == "-": + file = sys.stdin + else: + if not os.path.exists(args.file): + print("ERROR: file " + args.file + " not found") + sys.exit(1) + file = open(args.file, "r") + + addr2line = os.path.join(os.path.abspath(os.path.expanduser(args.tool)), + "bin/xtensa-" + PLATFORMS[args.platform] + "-elf-addr2line") + if not os.path.exists(addr2line): + print("ERROR: addr2line not found (" + addr2line + ")") + + elf_file = os.path.abspath(os.path.expanduser(args.elf)) + if not os.path.exists(elf_file): + print("ERROR: elf file not found (" + elf_file + ")") + + parser = ExceptionDataParser() + resolver = AddressResolver(addr2line, elf_file) + + parser.parse_file(file, args.stack_only) + resolver.fill(parser) + + print_result(parser, resolver, args.full, args.stack_only) diff --git a/scripts/rename_fw.py b/scripts/rename_fw.py index 7b4d42299..42a11881a 100755 --- a/scripts/rename_fw.py +++ b/scripts/rename_fw.py @@ -1,8 +1,26 @@ #!/usr/bin/env python -from subprocess import call -import os +import re Import("env") -# see http://docs.platformio.org/en/latest/projectconf/advanced_scripting.html#before-pre-and-after-post-actions -# env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION")) -env.Replace(PROGNAME="firmware_%s" % env['BOARD']) +bag = {} +exprs = [ + (re.compile(r'^#define APP_VERSION\s+"(\S+)"'), 'app_version'), + (re.compile(r'^#define APP_NAME\s+"(\S+)"'), 'app_name'), + (re.compile(r'^#define APP_HOSTNAME\s+"(\S+)"'), 'app_hostname') +] +with open('./src/version.h', 'r') as f: + for l in f.readlines(): + for expr, var in exprs: + m = expr.match(l) + if m and len(m.groups()) > 0: + bag[var] = m.group(1) + +app_version = bag.get('app_version') +app_name = bag.get('app_name') +app_hostname = bag.get('app_hostname') + +board = env['BOARD'] +branch = env['PIOENV'] + +# build filename, replacing . with _ for the version +env.Replace(PROGNAME="firmware_%s" % branch + "_" + app_version.replace(".", "_")) diff --git a/src/ds18.cpp b/src/ds18.cpp index 547eb88cd..37aa6540b 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -11,7 +11,7 @@ std::vector _devices; DS18::DS18() { - _wire = NULL; + _wire = nullptr; _count = 0; _gpio = GPIO_NONE; _parasite = 0; diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index 27e0518ab..5733a8367 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -37,11 +37,11 @@ DS18 ds18; #define DEFAULT_HEATINGCIRCUIT 1 // default to HC1 for thermostats that support multiple heating circuits like the RC35 // timers, all values are in seconds -#define DEFAULT_PUBLISHWAIT 120 // every 2 minutes publish MQTT values, including Dallas sensors +#define DEFAULT_PUBLISHTIME 120 // every 2 minutes publish MQTT values, including Dallas sensors Ticker publishValuesTimer; Ticker publishSensorValuesTimer; -#define SYSTEMCHECK_TIME 10 // every 10 seconds check if EMS can be reached +#define SYSTEMCHECK_TIME 30 // every 30 seconds check if EMS can be reached Ticker systemCheckTimer; #define REGULARUPDATES_TIME 60 // every minute a call is made to fetch data from EMS devices manually @@ -78,7 +78,7 @@ typedef struct { bool shower_alert; // true if we want the alert of cold water bool led; // LED on/off bool listen_mode; // stop automatic Tx on/off - uint16_t publish_wait; // frequency of MQTT publish in seconds + uint16_t publish_time; // frequency of MQTT publish in seconds uint8_t led_gpio; // pin for LED uint8_t dallas_gpio; // pin for attaching external dallas temperature sensors bool dallas_parasite; // on/off is using parasite @@ -93,21 +93,22 @@ typedef struct { bool doingColdShot; // true if we've just sent a jolt of cold water } _EMSESP_Shower; -command_t project_cmds[] = { +static const command_t project_cmds[] PROGMEM = { {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, "listen_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"}, - {true, "heating_circuit <1 | 2>", "set the thermostat HC to work with if using multiple heating circuits"}, + {true, "led_gpio ", "set the LED pin. Default is the onboard LED 2. For external D1 use 5"}, + {true, "dallas_gpio ", "set the external Dallas temperature sensors pin. Default is 14 for D5"}, + {true, "dallas_parasite ", "set to on if powering Dallas sesnsors via parasite power"}, + {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, "listen_mode ", "when set to on all automatic Tx are disabled"}, + {true, "shower_timer ", "send MQTT notification on all shower durations"}, + {true, "shower_alert ", "stop hot water to send 3 cold burst warnings after max shower time is exceeded"}, + {true, "publish_time ", "set frequency for publishing data to MQTT (0=off)"}, + {true, "heating_circuit <1 | 2>", "set the main thermostat HC to work with (if using multiple heating circuits)"}, + {true, "tx_mode ", "changes Tx logic. 0=ems 1.0, 1=ems+, 2=generic (experimental!), 3=HT3"}, - {false, "info", "show data captured on the EMS bus"}, + {false, "info", "show current captured on the devices"}, {false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, #ifdef TESTS @@ -132,6 +133,7 @@ command_t project_cmds[] = { {false, "boiler comfort ", "set boiler warm water comfort setting"} }; +uint8_t _project_cmds_count = ArraySize(project_cmds); // store for overall system status _EMSESP_Status EMSESP_Status; @@ -139,7 +141,7 @@ _EMSESP_Shower EMSESP_Shower; // logging messages with fixed strings void myDebugLog(const char * s) { - if (ems_getLogging() >= EMS_SYS_LOGGING_BASIC) { + if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { myDebug(s); } } @@ -173,11 +175,11 @@ char * _bool_to_char(char * s, uint8_t value) { } // convert short (two bytes) to text string -// decimals: 0 = no division, 1=divide value by 10, 10=divide value by 100 +// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 // 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 decimals = 1) { // remove errors or invalid values - if (abs(value) >= EMS_VALUE_SHORT_NOTSET) { + if (value == EMS_VALUE_SHORT_NOTSET) { strlcpy(s, "?", 10); return (s); } @@ -193,18 +195,59 @@ char * _short_to_char(char * s, int16_t value, uint8_t decimals = 1) { // check for negative values if (value < 0) { strlcpy(s, "-", 10); - value = abs(value); + value *= -1; // convert to positive } - strlcpy(s, ltoa(value / (decimals * 10), s2, 10), 10); - strlcat(s, ".", 10); - strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10); + if (decimals == 2) { + // divide by 2 + strlcpy(s, ltoa(value / 2, s2, 10), 10); + strlcat(s, ".", 10); + strlcat(s, ((value & 0x01) ? "5" : "0"), 10); + + } else { + strlcpy(s, ltoa(value / (decimals * 10), s2, 10), 10); + strlcat(s, ".", 10); + strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10); + } return s; } -// takes a short value (2 bytes), converts to a fraction -// decimals: 0 = no division, 1=divide value by 10, 10=divide value by 100 +// convert short (two bytes) to text string +// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 +char * _ushort_to_char(char * s, uint16_t value, uint8_t decimals = 1) { + // remove errors or invalid values + if (value == EMS_VALUE_USHORT_NOTSET) { + strlcpy(s, "?", 10); + return (s); + } + + // just print + if (decimals == 0) { + ltoa(value, s, 10); + return (s); + } + + // do floating point + char s2[10] = {0}; + + if (decimals == 2) { + // divide by 2 + strlcpy(s, ltoa(value / 2, s2, 10), 10); + strlcat(s, ".", 10); + strlcat(s, ((value & 0x01) ? "5" : "0"), 10); + + } else { + strlcpy(s, ltoa(value / (decimals * 10), s2, 10), 10); + strlcat(s, ".", 10); + strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10); + } + + return s; +} + +// takes a signed short value (2 bytes), converts to a fraction +// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals = 1) { static char buffer[200] = {0}; static char s[20] = {0}; @@ -214,7 +257,26 @@ void _renderShortValue(const char * prefix, const char * postfix, int16_t value, strlcat(buffer, _short_to_char(s, value, decimals), sizeof(buffer)); - if (postfix != NULL) { + if (postfix != nullptr) { + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); + } + + myDebug(buffer); +} + +// takes a unsigned short value (2 bytes), converts to a fraction +// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100 +void _renderUShortValue(const char * prefix, const char * postfix, uint16_t value, uint8_t decimals = 1) { + static char buffer[200] = {0}; + static char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + + strlcat(buffer, _ushort_to_char(s, value, decimals), sizeof(buffer)); + + if (postfix != nullptr) { strlcat(buffer, " ", sizeof(buffer)); strlcat(buffer, postfix, sizeof(buffer)); } @@ -266,7 +328,7 @@ void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, u strlcat(buffer, _int_to_char(s, value, div), sizeof(buffer)); - if (postfix != NULL) { + if (postfix != nullptr) { strlcat(buffer, " ", sizeof(buffer)); strlcat(buffer, postfix, sizeof(buffer)); } @@ -288,7 +350,7 @@ void _renderLongValue(const char * prefix, const char * postfix, uint32_t value) strlcat(buffer, ltoa(value, s, 10), sizeof(buffer)); } - if (postfix != NULL) { + if (postfix != nullptr) { strlcat(buffer, " ", sizeof(buffer)); strlcat(buffer, postfix, sizeof(buffer)); } @@ -309,6 +371,38 @@ void _renderBoolValue(const char * prefix, uint8_t value) { myDebug(buffer); } +// figures out the thermostat mode +// returns 0xFF=unknown, 0=low, 1=manual, 2=auto, 3=night, 4=day +uint8_t _getThermostatMode() { + int thermoMode = EMS_VALUE_INT_NOTSET; + + if (ems_getThermostatModel() == EMS_MODEL_RC20) { + if (EMS_Thermostat.mode == 0) { + thermoMode = 0; // low + } else if (EMS_Thermostat.mode == 1) { + thermoMode = 1; // manual + } else if (EMS_Thermostat.mode == 2) { + thermoMode = 2; // auto + } + } else if (ems_getThermostatModel() == EMS_MODEL_RC300) { + if (EMS_Thermostat.mode == 0) { + thermoMode = 1; // manual + } else if (EMS_Thermostat.mode == 1) { + thermoMode = 2; // auto + } + } else { // default for all thermostats + if (EMS_Thermostat.mode == 0) { + thermoMode = 3; // night + } else if (EMS_Thermostat.mode == 1) { + thermoMode = 4; // day + } else if (EMS_Thermostat.mode == 2) { + thermoMode = 2; // auto + } + } + + return thermoMode; +} + // Show command - display stats on an 's' command void showInfo() { // General stats from EMS bus @@ -323,6 +417,8 @@ void showInfo() { myDebug_P(PSTR(" System logging set to Verbose")); } else if (sysLog == EMS_SYS_LOGGING_THERMOSTAT) { myDebug_P(PSTR(" System logging set to Thermostat only")); + } else if (sysLog == EMS_SYS_LOGGING_SOLARMODULE) { + myDebug_P(PSTR(" System logging set to Solar Module only")); } else { myDebug_P(PSTR(" System logging set to None")); } @@ -332,9 +428,10 @@ void showInfo() { myDebug_P(PSTR(" %d external temperature sensor%s found"), EMSESP_Status.dallas_sensors, (EMSESP_Status.dallas_sensors == 1) ? "" : "s"); } - myDebug_P(PSTR(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s"), - (ems_getThermostatEnabled() ? "enabled" : "disabled"), + myDebug_P(PSTR(" Boiler is %s, Thermostat is %s, Solar Module is %s, Shower Timer is %s, Shower Alert is %s"), (ems_getBoilerEnabled() ? "enabled" : "disabled"), + (ems_getThermostatEnabled() ? "enabled" : "disabled"), + (ems_getSolarModuleEnabled() ? "enabled" : "disabled"), ((EMSESP_Status.shower_timer) ? "enabled" : "disabled"), ((EMSESP_Status.shower_alert) ? "enabled" : "disabled")); @@ -388,7 +485,7 @@ void showInfo() { _renderIntValue("Warm Water desired temperature", "C", EMS_Boiler.wWDesiredTemp); // UBAMonitorWWMessage - _renderShortValue("Warm Water current temperature", "C", EMS_Boiler.wWCurTmp); + _renderUShortValue("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) { @@ -401,8 +498,8 @@ void showInfo() { // UBAMonitorFast _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); - _renderShortValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); - _renderShortValue("Return temperature", "C", EMS_Boiler.retTemp); + _renderUShortValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); + _renderUShortValue("Return temperature", "C", EMS_Boiler.retTemp); _renderBoolValue("Gas", EMS_Boiler.burnGas); _renderBoolValue("Boiler pump", EMS_Boiler.heatPmp); _renderBoolValue("Fan", EMS_Boiler.fanWork); @@ -424,10 +521,10 @@ void showInfo() { _renderIntValue("Boiler circuit pump modulation min power", "%", EMS_Boiler.pump_mod_min); // UBAMonitorSlow - if (EMS_Boiler.extTemp != (int16_t)EMS_VALUE_SHORT_NOTSET) { + if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) { _renderShortValue("Outside temperature", "C", EMS_Boiler.extTemp); } - _renderShortValue("Boiler temperature", "C", EMS_Boiler.boilTemp); + _renderUShortValue("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) { @@ -450,24 +547,32 @@ void showInfo() { } // For SM10/SM100 Solar Module - if (EMS_Other.SM) { + if (ems_getSolarModuleEnabled()) { myDebug_P(PSTR("")); // newline myDebug_P(PSTR("%sSolar Module stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - _renderShortValue("Collector temperature", "C", EMS_Other.SMcollectorTemp); - _renderShortValue("Bottom temperature", "C", EMS_Other.SMbottomTemp); - _renderIntValue("Pump modulation", "%", EMS_Other.SMpumpModulation); - _renderBoolValue("Pump active", EMS_Other.SMpump); - _renderShortValue("Energy Last Hour", "Wh", EMS_Other.SMEnergyLastHour, 1); // *10 - _renderShortValue("Energy Today", "Wh", EMS_Other.SMEnergyToday, 0); - _renderShortValue("Energy Total", "kWH", EMS_Other.SMEnergyTotal, 1); // *10 + myDebug_P(PSTR(" Solar Module: %s"), ems_getSolarModuleDescription(buffer_type)); + _renderShortValue("Collector temperature", "C", EMS_SolarModule.collectorTemp); + _renderShortValue("Bottom temperature", "C", EMS_SolarModule.bottomTemp); + _renderIntValue("Pump modulation", "%", EMS_SolarModule.pumpModulation); + _renderBoolValue("Pump active", EMS_SolarModule.pump); + if (EMS_SolarModule.pumpWorkMin != EMS_VALUE_LONG_NOTSET) { + myDebug_P(PSTR(" Pump working time: %d days %d hours %d minutes"), + EMS_SolarModule.pumpWorkMin / 1440, + (EMS_SolarModule.pumpWorkMin % 1440) / 60, + EMS_SolarModule.pumpWorkMin % 60); + } + _renderUShortValue("Energy Last Hour", "Wh", EMS_SolarModule.EnergyLastHour, 1); // *10 + _renderUShortValue("Energy Today", "Wh", EMS_SolarModule.EnergyToday, 0); + _renderUShortValue("Energy Total", "kWH", EMS_SolarModule.EnergyTotal, 1); // *10 } // For HeatPumps - if (EMS_Other.HP) { + if (ems_getHeatPumpEnabled()) { myDebug_P(PSTR("")); // newline myDebug_P(PSTR("%sHeat Pump stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - _renderIntValue("Pump modulation", "%", EMS_Other.HPModulation); - _renderIntValue("Pump speed", "%", EMS_Other.HPSpeed); + myDebug_P(PSTR(" Solar Module: %s"), ems_getHeatPumpDescription(buffer_type)); + _renderIntValue("Pump modulation", "%", EMS_HeatPump.HPModulation); + _renderIntValue("Pump speed", "%", EMS_HeatPump.HPSpeed); } // Thermostat stats @@ -477,26 +582,18 @@ void showInfo() { myDebug_P(PSTR(" Thermostat: %s"), ems_getThermostatDescription(buffer_type)); // Render Current & Setpoint Room Temperature - if ((ems_getThermostatModel() == EMS_MODEL_EASY)) { + if (ems_getThermostatModel() == EMS_MODEL_EASY) { // Temperatures are *100 _renderShortValue("Set room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 10); // *100 _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 10); // *100 - } - else if ((ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100)) { + } else if ((ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100)) { // Temperatures are *10 _renderShortValue("Set room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 1); // *10 _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 1); // *10 - } - else { + } 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.setpoint_roomTemp = EMS_VALUE_INT_NOTSET; - } - if (EMS_Thermostat.curr_roomTemp <= 0) { - EMS_Thermostat.curr_roomTemp = EMS_VALUE_INT_NOTSET; - } - _renderShortValue("Setpoint room temperature", "C", (EMS_Thermostat.setpoint_roomTemp * 5), 1); // convert to a single byte * 2 (1 decimal but the value is original) - _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 1); // is *10 + _renderShortValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 2); // convert to a single byte * 2 + _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 1); // is *10 } // Render Day/Night/Holiday Temperature @@ -507,23 +604,30 @@ void showInfo() { } // Render Thermostat Date & Time - myDebug_P(PSTR(" Thermostat time is %02d:%02d:%02d %d/%d/%d"), - EMS_Thermostat.hour, - EMS_Thermostat.minute, - EMS_Thermostat.second, - EMS_Thermostat.day, - EMS_Thermostat.month, - EMS_Thermostat.year + 2000); + // not for EASY + if ((ems_getThermostatModel() != EMS_MODEL_EASY)) { + myDebug_P(PSTR(" Thermostat time is %02d:%02d:%02d %d/%d/%d"), + EMS_Thermostat.hour, + EMS_Thermostat.minute, + EMS_Thermostat.second, + EMS_Thermostat.day, + EMS_Thermostat.month, + EMS_Thermostat.year + 2000); + } - // Render Termostat Mode - if (EMS_Thermostat.mode == 0) { + // Render Termostat Mode, if we have a mode + uint8_t thermoMode = _getThermostatMode(); // 0xFF=unknown, 0=low, 1=manual, 2=auto, 3=night, 4=day + + if (thermoMode == 0) { myDebug_P(PSTR(" Mode is set to low")); - } else if (EMS_Thermostat.mode == 1) { + } else if (thermoMode == 1) { myDebug_P(PSTR(" Mode is set to manual")); - } else if (EMS_Thermostat.mode == 2) { + } else if (thermoMode == 2) { myDebug_P(PSTR(" Mode is set to auto")); - } else { - myDebug_P(PSTR(" Mode is set to ?")); + } else if (thermoMode == 3) { + myDebug_P(PSTR(" Mode is set to night")); + } else if (thermoMode == 4) { + myDebug_P(PSTR(" Mode is set to day")); } } @@ -579,6 +683,8 @@ void publishSensorValues() { } } + + // 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 @@ -598,7 +704,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) + static uint32_t previousSMPublishCRC = 0; // CRC check for Solar Module values (e.g. SM10) JsonObject rootBoiler = doc.to(); @@ -621,19 +727,19 @@ void publishValues(bool force) { if (EMS_Boiler.pumpMod != EMS_VALUE_INT_NOTSET) rootBoiler["pumpMod"] = EMS_Boiler.pumpMod; - if (abs(EMS_Boiler.extTemp) < EMS_VALUE_SHORT_NOTSET) + if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) rootBoiler["outdoorTemp"] = (double)EMS_Boiler.extTemp / 10; - if (abs(EMS_Boiler.wWCurTmp) < EMS_VALUE_SHORT_NOTSET) + if (EMS_Boiler.wWCurTmp != EMS_VALUE_USHORT_NOTSET) rootBoiler["wWCurTmp"] = (double)EMS_Boiler.wWCurTmp / 10; - if (abs(EMS_Boiler.wWCurFlow) != EMS_VALUE_INT_NOTSET) + if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET) rootBoiler["wWCurFlow"] = (double)EMS_Boiler.wWCurFlow / 10; - if (abs(EMS_Boiler.curFlowTemp) < EMS_VALUE_SHORT_NOTSET) + if (EMS_Boiler.curFlowTemp != EMS_VALUE_USHORT_NOTSET) rootBoiler["curFlowTemp"] = (double)EMS_Boiler.curFlowTemp / 10; - if (abs(EMS_Boiler.retTemp) < EMS_VALUE_SHORT_NOTSET) + if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) rootBoiler["retTemp"] = (double)EMS_Boiler.retTemp / 10; - if (abs(EMS_Boiler.sysPress) != EMS_VALUE_INT_NOTSET) + if (EMS_Boiler.sysPress != EMS_VALUE_INT_NOTSET) rootBoiler["sysPress"] = (double)EMS_Boiler.sysPress / 10; - if (abs(EMS_Boiler.boilTemp) < EMS_VALUE_SHORT_NOTSET) + if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) rootBoiler["boilTemp"] = (double)EMS_Boiler.boilTemp / 10; if (EMS_Boiler.wWActivated != EMS_VALUE_INT_NOTSET) @@ -657,6 +763,14 @@ void publishValues(bool force) { if (EMS_Boiler.wWHeat != EMS_VALUE_INT_NOTSET) rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); + // **** also add burnStarts, burnWorkMin, heatWorkMin + if (abs(EMS_Boiler.burnStarts) != EMS_VALUE_LONG_NOTSET) + rootBoiler["burnStarts"] = (double)EMS_Boiler.burnStarts; + if (abs(EMS_Boiler.burnWorkMin) != EMS_VALUE_LONG_NOTSET) + rootBoiler["burnWorkMin"] = (double)EMS_Boiler.burnWorkMin; + if (abs(EMS_Boiler.heatWorkMin) != EMS_VALUE_LONG_NOTSET) + rootBoiler["heatWorkMin"] = (double)EMS_Boiler.heatWorkMin; + rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; @@ -686,11 +800,8 @@ 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)) - return; - + // only send thermostat values if we actually have them + if (ems_getThermostatEnabled() && ((EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET) && (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET))) { // build new json object doc.clear(); JsonObject rootThermostat = doc.to(); @@ -698,16 +809,20 @@ void publishValues(bool force) { rootThermostat[THERMOSTAT_HC] = _int_to_char(s, EMSESP_Status.heating_circuit); // different logic depending on thermostat types - if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100)) { - if (abs(EMS_Thermostat.setpoint_roomTemp) < EMS_VALUE_SHORT_NOTSET) + if (ems_getThermostatModel() == EMS_MODEL_EASY) { + if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) + rootThermostat[THERMOSTAT_SELTEMP] = (double)EMS_Thermostat.setpoint_roomTemp / 100; + if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET) + rootThermostat[THERMOSTAT_CURRTEMP] = (double)EMS_Thermostat.curr_roomTemp / 100; + } else if ((ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100)) { + if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) rootThermostat[THERMOSTAT_SELTEMP] = (double)EMS_Thermostat.setpoint_roomTemp / 10; - if (abs(EMS_Thermostat.curr_roomTemp) < EMS_VALUE_SHORT_NOTSET) + if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET) rootThermostat[THERMOSTAT_CURRTEMP] = (double)EMS_Thermostat.curr_roomTemp / 10; - } else { - if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_INT_NOTSET) + if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) rootThermostat[THERMOSTAT_SELTEMP] = (double)EMS_Thermostat.setpoint_roomTemp / 2; - if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_INT_NOTSET) + if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET) rootThermostat[THERMOSTAT_CURRTEMP] = (double)EMS_Thermostat.curr_roomTemp / 10; if (EMS_Thermostat.daytemp != EMS_VALUE_INT_NOTSET) @@ -724,23 +839,19 @@ void publishValues(bool force) { rootThermostat[THERMOSTAT_CIRCUITCALCTEMP] = EMS_Thermostat.circuitcalctemp; } - // RC20 has different mode settings - if (ems_getThermostatModel() == EMS_MODEL_RC20) { - if (EMS_Thermostat.mode == 0) { - rootThermostat[THERMOSTAT_MODE] = "low"; - } else if (EMS_Thermostat.mode == 1) { - rootThermostat[THERMOSTAT_MODE] = "manual"; - } else { - rootThermostat[THERMOSTAT_MODE] = "auto"; - } - } else { - if (EMS_Thermostat.mode == 0) { - rootThermostat[THERMOSTAT_MODE] = "night"; - } else if (EMS_Thermostat.mode == 1) { - rootThermostat[THERMOSTAT_MODE] = "day"; - } else { - rootThermostat[THERMOSTAT_MODE] = "auto"; - } + uint8_t thermoMode = _getThermostatMode(); // 0xFF=unknown, 0=low, 1=manual, 2=auto, 3=night, 4=day + + // Termostat Mode + if (thermoMode == 0) { + rootThermostat[THERMOSTAT_MODE] = "low"; + } else if (thermoMode == 1) { + rootThermostat[THERMOSTAT_MODE] = "manual"; + } else if (thermoMode == 2) { + rootThermostat[THERMOSTAT_MODE] = "auto"; + } else if (thermoMode == 3) { + rootThermostat[THERMOSTAT_MODE] = "night"; + } else if (thermoMode == 4) { + rootThermostat[THERMOSTAT_MODE] = "day"; } data[0] = '\0'; // reset data for next package @@ -761,35 +872,37 @@ void publishValues(bool force) { } } - // handle the other values separately - // For SM10 and SM100 Solar Modules - if (EMS_Other.SM) { + if (ems_getSolarModuleEnabled()) { // build new json object doc.clear(); JsonObject rootSM = doc.to(); - if (abs(EMS_Other.SMcollectorTemp) < EMS_VALUE_SHORT_NOTSET) - rootSM[SM_COLLECTORTEMP] = (double)EMS_Other.SMcollectorTemp / 10; + if (EMS_SolarModule.collectorTemp != EMS_VALUE_SHORT_NOTSET) + rootSM[SM_COLLECTORTEMP] = (double)EMS_SolarModule.collectorTemp / 10; - if (abs(EMS_Other.SMbottomTemp) < EMS_VALUE_SHORT_NOTSET) - rootSM[SM_BOTTOMTEMP] = (double)EMS_Other.SMbottomTemp / 10; + if (EMS_SolarModule.bottomTemp != EMS_VALUE_SHORT_NOTSET) + rootSM[SM_BOTTOMTEMP] = (double)EMS_SolarModule.bottomTemp / 10; - if (EMS_Other.SMpumpModulation != EMS_VALUE_INT_NOTSET) - rootSM[SM_PUMPMODULATION] = EMS_Other.SMpumpModulation; + if (EMS_SolarModule.pumpModulation != EMS_VALUE_INT_NOTSET) + rootSM[SM_PUMPMODULATION] = EMS_SolarModule.pumpModulation; - if (EMS_Other.SMpump != EMS_VALUE_INT_NOTSET) { - rootSM[SM_PUMP] = _bool_to_char(s, EMS_Other.SMpump); + if (EMS_SolarModule.pump != EMS_VALUE_INT_NOTSET) { + rootSM[SM_PUMP] = _bool_to_char(s, EMS_SolarModule.pump); } - if (abs(EMS_Other.SMEnergyLastHour) < EMS_VALUE_SHORT_NOTSET) - rootSM[SM_ENERGYLASTHOUR] = (double)EMS_Other.SMEnergyLastHour / 10; + if (EMS_SolarModule.pumpWorkMin != EMS_VALUE_LONG_NOTSET) { + rootSM[SM_PUMPWORKMIN] = (double)EMS_SolarModule.pumpWorkMin; + } - if (abs(EMS_Other.SMEnergyToday) < EMS_VALUE_SHORT_NOTSET) - rootSM[SM_ENERGYTODAY] = EMS_Other.SMEnergyToday; + if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_USHORT_NOTSET) + rootSM[SM_ENERGYLASTHOUR] = (double)EMS_SolarModule.EnergyLastHour / 10; - if (abs(EMS_Other.SMEnergyTotal) < EMS_VALUE_SHORT_NOTSET) - rootSM[SM_ENERGYTOTAL] = (double)EMS_Other.SMEnergyTotal / 10; + if (EMS_SolarModule.EnergyToday != EMS_VALUE_USHORT_NOTSET) + rootSM[SM_ENERGYTODAY] = EMS_SolarModule.EnergyToday; + + if (EMS_SolarModule.EnergyTotal != EMS_VALUE_USHORT_NOTSET) + rootSM[SM_ENERGYTOTAL] = (double)EMS_SolarModule.EnergyTotal / 10; data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); @@ -800,8 +913,8 @@ void publishValues(bool force) { crc.update(data[i]); } fchecksum = crc.finalize(); - if ((previousOtherPublishCRC != fchecksum) || force) { - previousOtherPublishCRC = fchecksum; + if ((previousSMPublishCRC != fchecksum) || force) { + previousSMPublishCRC = fchecksum; myDebugLog("Publishing SM data via MQTT"); // send values via MQTT @@ -810,16 +923,16 @@ void publishValues(bool force) { } // handle HeatPump - if (EMS_Other.HP) { + if (ems_getHeatPumpEnabled()) { // build new json object doc.clear(); JsonObject rootSM = doc.to(); - if (EMS_Other.HPModulation != EMS_VALUE_INT_NOTSET) - rootSM[HP_PUMPMODULATION] = EMS_Other.HPModulation; + if (EMS_HeatPump.HPModulation != EMS_VALUE_INT_NOTSET) + rootSM[HP_PUMPMODULATION] = EMS_HeatPump.HPModulation; - if (EMS_Other.HPSpeed != EMS_VALUE_INT_NOTSET) - rootSM[HP_PUMPSPEED] = EMS_Other.HPSpeed; + if (EMS_HeatPump.HPSpeed != EMS_VALUE_INT_NOTSET) + rootSM[HP_PUMPSPEED] = EMS_HeatPump.HPSpeed; data[0] = '\0'; // reset data for next package serializeJson(doc, data, sizeof(data)); @@ -847,7 +960,7 @@ void set_showerAlert() { // used to read the next string from an input buffer and convert to an 8 bit int uint8_t _readIntNumber() { - char * numTextPtr = strtok(NULL, ", \n"); + char * numTextPtr = strtok(nullptr, ", \n"); if (numTextPtr == nullptr) { return 0; } @@ -856,25 +969,25 @@ uint8_t _readIntNumber() { // used to read the next string from an input buffer and convert to a double float _readFloatNumber() { - char * numTextPtr = strtok(NULL, ", \n"); + char * numTextPtr = strtok(nullptr, ", \n"); if (numTextPtr == nullptr) { return 0; } return atof(numTextPtr); } -// used to read the next string from an input buffer as a hex value and convert to an 8 bit int -uint8_t _readHexNumber() { - char * numTextPtr = strtok(NULL, ", \n"); +// used to read the next string from an input buffer as a hex value and convert to a 16 bit int +uint16_t _readHexNumber() { + char * numTextPtr = strtok(nullptr, ", \n"); if (numTextPtr == nullptr) { return 0; } - return (uint8_t)strtol(numTextPtr, 0, 16); + return (uint16_t)strtol(numTextPtr, 0, 16); } // used to read the next string from an input buffer char * _readWord() { - char * word = strtok(NULL, ", \n"); + char * word = strtok(nullptr, ", \n"); return word; } @@ -888,15 +1001,15 @@ void do_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()) { + if ((ems_getBusConnected()) && myESP.isMQTTConnected() && EMSESP_Status.publish_time != 0) { publishValues(true); // force publish } } // 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 +// when ESP is booting up, ignore this as the LED is being used for something else void do_ledcheck() { - if (EMSESP_Status.led) { + if ((EMSESP_Status.led) && (myESP.getSystemBootStatus() == MYESP_BOOTSTATUS_BOOTED)) { if (ems_getBusConnected()) { digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off } else { @@ -908,7 +1021,7 @@ void do_ledcheck() { // Thermostat scan void do_scanThermostat() { - if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + if (ems_getBusConnected()) { myDebug_P(PSTR("> Scanning thermostat message type #0x%02X..."), scanThermostat_count); ems_doReadCommand(scanThermostat_count, EMS_Thermostat.device_id); scanThermostat_count++; @@ -917,7 +1030,7 @@ void do_scanThermostat() { // do a system health check every now and then to see if we all connections void do_systemCheck() { - if ((!ems_getBusConnected()) && (!myESP.getUseSerial())) { + if (!ems_getBusConnected()) { myDebug_P(PSTR("Error! Unable to read the EMS bus.")); } } @@ -925,18 +1038,20 @@ void do_systemCheck() { // 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())) { + if (ems_getBusConnected() && !ems_getTxDisabled()) { myDebugLog("Requesting scheduled EMS device data"); ems_getThermostatValues(); ems_getBoilerValues(); - ems_getOtherValues(); + ems_getSolarModuleValues(); + } else { + myDebugLog("System is either not connect to the EMS bus or listen_mode is enabled"); } } // stop devices scan and restart all other timers void stopDeviceScan() { - publishValuesTimer.attach(EMSESP_Status.publish_wait, do_publishValues); // post MQTT EMS values - publishSensorValuesTimer.attach(EMSESP_Status.publish_wait, 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 systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if Boiler is online scanThermostat_count = 0; @@ -954,7 +1069,7 @@ void do_scanDevices() { return; } - if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + if (ems_getBusConnected()) { ems_doReadCommand(EMS_TYPE_Version, scanDevices_count++); // ask for version } } @@ -1018,56 +1133,27 @@ void runUnitTest(uint8_t test_num) { // callback for loading/saving settings to the file system (SPIFFS) bool FSCallback(MYESP_FSACTION action, const JsonObject json) { if (action == MYESP_FSACTION_LOAD) { - bool recreate_config = true; + EMSESP_Status.led = json["led"]; + EMSESP_Status.led_gpio = json["led_gpio"] | EMSESP_LED_GPIO; + EMSESP_Status.dallas_gpio = json["dallas_gpio"] | EMSESP_DALLAS_GPIO; + EMSESP_Status.dallas_parasite = json["dallas_parasite"] | EMSESP_DALLAS_PARASITE; - // led - EMSESP_Status.led = json["led"]; + EMS_Thermostat.device_id = json["thermostat_type"] | EMSESP_THERMOSTAT_TYPE; + EMS_Boiler.device_id = json["boiler_type"] | EMSESP_BOILER_TYPE; - // led_gpio - if (!(EMSESP_Status.led_gpio = json["led_gpio"])) { - EMSESP_Status.led_gpio = EMSESP_LED_GPIO; // default value - } + EMSESP_Status.shower_timer = json["shower_timer"]; + EMSESP_Status.shower_alert = json["shower_alert"]; + EMSESP_Status.publish_time = json["publish_time"] | DEFAULT_PUBLISHTIME; - // dallas_gpio - if (!(EMSESP_Status.dallas_gpio = json["dallas_gpio"])) { - EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; // default value - } + ems_setTxMode(json["tx_mode"]); - // dallas_parasite - EMSESP_Status.dallas_parasite = json["dallas_parasite"]; - - // thermostat_type - if (!(EMS_Thermostat.device_id = json["thermostat_type"])) { - EMS_Thermostat.device_id = EMSESP_THERMOSTAT_TYPE; // set default - } - - // boiler_type - if (!(EMS_Boiler.device_id = json["boiler_type"])) { - EMS_Boiler.device_id = EMSESP_BOILER_TYPE; // set default - } - - // listen mode EMSESP_Status.listen_mode = json["listen_mode"]; ems_setTxDisabled(EMSESP_Status.listen_mode); - // shower_timer - EMSESP_Status.shower_timer = json["shower_timer"]; - - // shower_alert - EMSESP_Status.shower_alert = json["shower_alert"]; - - // publish_wait - if (!(EMSESP_Status.publish_wait = json["publish_wait"])) { - EMSESP_Status.publish_wait = DEFAULT_PUBLISHWAIT; // default value - } - - // heating_circuit - if (!(EMSESP_Status.heating_circuit = json["heating_circuit"])) { - EMSESP_Status.heating_circuit = DEFAULT_HEATINGCIRCUIT; // default value - } + EMSESP_Status.heating_circuit = json["heating_circuit"] | DEFAULT_HEATINGCIRCUIT; ems_setThermostatHC(EMSESP_Status.heating_circuit); - return recreate_config; // return false if some settings are missing and we need to rebuild the file + return true; // return false if some settings are missing and we need to rebuild the file } if (action == MYESP_FSACTION_SAVE) { @@ -1080,8 +1166,9 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { json["listen_mode"] = EMSESP_Status.listen_mode; json["shower_timer"] = EMSESP_Status.shower_timer; json["shower_alert"] = EMSESP_Status.shower_alert; - json["publish_wait"] = EMSESP_Status.publish_wait; + json["publish_time"] = EMSESP_Status.publish_time; json["heating_circuit"] = EMSESP_Status.heating_circuit; + json["tx_mode"] = ems_getTxMode(); return true; } @@ -1194,9 +1281,9 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } } - // publish_wait - if ((strcmp(setting, "publish_wait") == 0) && (wc == 2)) { - EMSESP_Status.publish_wait = atoi(value); + // publish_time + if ((strcmp(setting, "publish_time") == 0) && (wc == 2)) { + EMSESP_Status.publish_time = atoi(value); ok = true; } @@ -1212,6 +1299,11 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } } + // tx delay/ tx mode + if (((strcmp(setting, "tx_mode") == 0) || (strcmp(setting, "tx_delay") == 0)) && (wc == 2)) { + ems_setTxMode(atoi(value)); + ok = true; + } } if (action == MYESP_FSACTION_LIST) { @@ -1237,12 +1329,47 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c myDebug_P(PSTR(" listen_mode=%s"), EMSESP_Status.listen_mode ? "on" : "off"); myDebug_P(PSTR(" shower_timer=%s"), EMSESP_Status.shower_timer ? "on" : "off"); myDebug_P(PSTR(" shower_alert=%s"), EMSESP_Status.shower_alert ? "on" : "off"); - myDebug_P(PSTR(" publish_wait=%d"), EMSESP_Status.publish_wait); + myDebug_P(PSTR(" publish_time=%d"), EMSESP_Status.publish_time); + myDebug_P(PSTR(" tx_mode=%d"), ems_getTxMode()); } return ok; } +// print settings +void _showCommands(uint8_t event) { + bool mode = (event == TELNET_EVENT_SHOWSET); // show set commands or normal commands + command_t cmd; + + // find the longest key length so we can right-align the text + uint8_t max_len = 0; + uint8_t i; + for (i = 0; i < _project_cmds_count; i++) { + memcpy_P(&cmd, &project_cmds[i], sizeof(cmd)); + if ((strlen(cmd.key) > max_len) && (cmd.set == mode)) { + max_len = strlen(cmd.key); + } + } + + char line[200] = {0}; + for (i = 0; i < _project_cmds_count; i++) { + memcpy_P(&cmd, &project_cmds[i], sizeof(cmd)); + if (cmd.set == mode) { + if (event == TELNET_EVENT_SHOWSET) { + strlcpy(line, " set ", sizeof(line)); + } else { + strlcpy(line, "* ", sizeof(line)); + } + strlcat(line, cmd.key, sizeof(line)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(cmd.key)); j++) { // account for longest string length + strlcat(line, " ", sizeof(line)); // padding + } + strlcat(line, cmd.description, sizeof(line)); + myDebug(line); // print the line + } + } +} + // call back when a telnet client connects or disconnects // we set the logging here void TelnetCallback(uint8_t event) { @@ -1250,6 +1377,8 @@ void TelnetCallback(uint8_t event) { ems_setLogging(EMS_SYS_LOGGING_DEFAULT); } else if (event == TELNET_EVENT_DISCONNECT) { ems_setLogging(EMS_SYS_LOGGING_NONE); + } else if ((event == TELNET_EVENT_SHOWCMD) || (event == TELNET_EVENT_SHOWSET)) { + _showCommands(event); } } @@ -1271,7 +1400,6 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } if (strcmp(first_cmd, "refresh") == 0) { - myDebug_P(PSTR("Fetching data from EMS devices...")); do_regularUpdates(); ok = true; } @@ -1330,6 +1458,9 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } else if (strcmp(second_cmd, "t") == 0) { ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); ok = true; + } else if (strcmp(second_cmd, "s") == 0) { + ems_setLogging(EMS_SYS_LOGGING_SOLARMODULE); + ok = true; } else if (strcmp(second_cmd, "r") == 0) { ems_setLogging(EMS_SYS_LOGGING_RAW); ok = true; @@ -1432,6 +1563,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { myESP.mqttSubscribe(TOPIC_BOILER_WWACTIVATED); myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); myESP.mqttSubscribe(TOPIC_BOILER_CMD_COMFORT); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_FLOWTEMP); myESP.mqttSubscribe(TOPIC_SHOWER_TIMER); myESP.mqttSubscribe(TOPIC_SHOWER_ALERT); myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT); @@ -1504,7 +1636,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { // wwActivated if (strcmp(topic, TOPIC_BOILER_WWACTIVATED) == 0) { - if (message[0] == '1' || strcmp(message, "on") == 0) { + if ((message[0] == '1' || strcmp(message, "on") == 0) || (strcmp(message, "auto") == 0)) { ems_setWarmWaterActivated(true); } else if (message[0] == '0' || strcmp(message, "off") == 0) { ems_setWarmWaterActivated(false); @@ -1531,6 +1663,13 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { } } + // boiler flowtemp setting + if (strcmp(topic, TOPIC_BOILER_CMD_FLOWTEMP) == 0) { + uint8_t t = atoi((char *)message); + myDebug_P(PSTR("MQTT topic: boiler flowtemp value %d"), t); + ems_setFlowTemp(t); + } + // shower timer if (strcmp(topic, TOPIC_SHOWER_TIMER) == 0) { if (message[0] == '1') { @@ -1558,11 +1697,47 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { } } +// web information for diagnostics +void WebCallback(char * body) { + strlcpy(body, "EMS stats:
", MYESP_MAXCHARBUFFER); + + if (ems_getBusConnected()) { + char s[10]; + strlcat(body, "EMS Bus is connected
", MYESP_MAXCHARBUFFER); + strlcat(body, "Rx: # successful read requests=", MYESP_MAXCHARBUFFER); + strlcat(body, itoa(EMS_Sys_Status.emsRxPgks, s, 10), MYESP_MAXCHARBUFFER); + strlcat(body, ", # CRC errors=", MYESP_MAXCHARBUFFER); + strlcat(body, itoa(EMS_Sys_Status.emxCrcErr, s, 10), MYESP_MAXCHARBUFFER); + if (ems_getTxCapable()) { + strlcat(body, "
Tx: # successful write requests=", MYESP_MAXCHARBUFFER); + strlcat(body, itoa(EMS_Sys_Status.emsTxPkgs, s, 10), MYESP_MAXCHARBUFFER); + } else { + strlcat(body, "
Tx: no signal

", MYESP_MAXCHARBUFFER); + } + + // show device list + strlcpy(body, "EMS devices found:
", MYESP_MAXCHARBUFFER); + + char buffer[MYESP_MAXCHARBUFFER] = {0}; + uint8_t num_devices = ems_printDevices_s(buffer, MYESP_MAXCHARBUFFER); + if (num_devices == 0) { + strlcat(body, "(any detected and compatible EMS devices will show up here)", MYESP_MAXCHARBUFFER); + } else { + strlcat(body, buffer, MYESP_MAXCHARBUFFER); + } + + } else { + strlcat(body, "Unable to establish a connection to the EMS Bus.", MYESP_MAXCHARBUFFER); + } +} + // Init callback, which is used to set functions and call methods after a wifi connection has been established void WIFICallback() { // This is where we enable the UART service to scan the incoming serial Tx/Rx bus signals // This is done after we have a WiFi signal to avoid any resource conflicts + system_uart_swap(); // TODO check + /* if (myESP.getUseSerial()) { myDebug_P(PSTR("Warning! EMS bus communication disabled when Serial mode enabled. Use 'set serial off' to start communication.")); } else { @@ -1573,6 +1748,7 @@ void WIFICallback() { ems_discoverModels(); } } + */ } // Initialize the boiler settings and shower settings @@ -1583,7 +1759,7 @@ void initEMSESP() { EMSESP_Status.shower_alert = false; EMSESP_Status.led = true; // LED is on by default EMSESP_Status.listen_mode = false; - EMSESP_Status.publish_wait = DEFAULT_PUBLISHWAIT; + EMSESP_Status.publish_time = DEFAULT_PUBLISHTIME; EMSESP_Status.timestamp = millis(); EMSESP_Status.dallas_sensors = 0; EMSESP_Status.led_gpio = EMSESP_LED_GPIO; @@ -1595,6 +1771,9 @@ void initEMSESP() { EMSESP_Shower.timerPause = 0; EMSESP_Shower.duration = 0; EMSESP_Shower.doingColdShot = false; + + // call ems.cpp's init function to set all the internal params + ems_init(); } /* @@ -1667,17 +1846,25 @@ void showerCheck() { // SETUP // void setup() { + // LA trigger create a small puls to show setup is starting... + INIT_MARKERS(0); + LA_PULSE(50); + + // GPIO15 has a pull down, so we must set it to HIGH + pinMode(15, OUTPUT); + digitalWrite(15, 1); + // init our own parameters initEMSESP(); // call ems.cpp's init function to set all the internal params ems_init(); - systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if Boiler is online + systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if EMS is reachable // set up myESP for Wifi, MQTT, MDNS and Telnet - myESP.setTelnet(project_cmds, ArraySize(project_cmds), TelnetCommandCallback, TelnetCallback); // set up Telnet commands - myESP.setWIFI(NULL, NULL, WIFICallback); + myESP.setTelnet(TelnetCommandCallback, TelnetCallback); // set up Telnet commands + myESP.setWIFI(NULL, NULL, WIFICallback); // empty ssid and password as we take this from the config file // MQTT host, username and password taken from the SPIFFS settings myESP.setMQTT( @@ -1689,15 +1876,31 @@ void setup() { // custom settings in SPIFFS myESP.setSettings(FSCallback, SettingsCallback); + // web custom settings + myESP.setWeb(WebCallback); + // start up all the services myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION); // at this point we have all the settings from our internall SPIFFS config file + // fire up the UART now + if (myESP.getUseSerial()) { + myDebug_P(PSTR("Warning! EMS bus communication disabled when Serial mode enabled. Use 'set serial off' to start communication.")); + } else { + Serial.println("Note: Serial output will now be disabled. Please use Telnet."); + Serial.flush(); + emsuart_init(); + myDebug_P(PSTR("[UART] Opened Rx/Tx connection")); + if (!EMSESP_Status.listen_mode) { + // go and find the boiler and thermostat types, if not in listen mode + ems_discoverModels(); + } + } // enable regular checks if not in test mode if (!EMSESP_Status.listen_mode) { - publishValuesTimer.attach(EMSESP_Status.publish_wait, do_publishValues); // post MQTT EMS values - publishSensorValuesTimer.attach(EMSESP_Status.publish_wait, 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 } @@ -1710,6 +1913,8 @@ void setup() { // check for Dallas sensors EMSESP_Status.dallas_sensors = ds18.setup(EMSESP_Status.dallas_gpio, EMSESP_Status.dallas_parasite); // returns #sensors + + systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if EMS is reachable } // diff --git a/src/ems.cpp b/src/ems.cpp index 33bbb2adb..b33c6c597 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -63,6 +63,7 @@ void _process_SM100Energy(_EMS_RxTelegram * EMS_RxTelegram); // ISM1 void _process_ISM1StatusMessage(_EMS_RxTelegram * EMS_RxTelegram); +void _process_ISM1Set(_EMS_RxTelegram * EMS_RxTelegram); // HeatPump HP void _process_HPMonitor1(_EMS_RxTelegram * EMS_RxTelegram); @@ -116,19 +117,22 @@ const _EMS_Type EMS_Types[] = { {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage}, {EMS_MODEL_UBA, EMS_TYPE_UBAParameterWW, "UBAParameterWW", _process_UBAParameterWW}, {EMS_MODEL_UBA, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", _process_UBATotalUptimeMessage}, - {EMS_MODEL_UBA, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", NULL}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", nullptr}, {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}, - {EMS_MODEL_OTHER, EMS_TYPE_SM100Monitor, "SM100Monitor", _process_SM100Monitor}, - {EMS_MODEL_OTHER, EMS_TYPE_SM100Status, "SM100Status", _process_SM100Status}, - {EMS_MODEL_OTHER, EMS_TYPE_SM100Status2, "SM100Status2", _process_SM100Status2}, - {EMS_MODEL_OTHER, EMS_TYPE_SM100Energy, "SM100Energy", _process_SM100Energy}, - {EMS_MODEL_OTHER, EMS_TYPE_HPMonitor1, "HeatPumpMonitor1", _process_HPMonitor1}, - {EMS_MODEL_OTHER, EMS_TYPE_HPMonitor2, "HeatPumpMonitor2", _process_HPMonitor2}, - {EMS_MODEL_OTHER, EMS_TYPE_ISM1StatusMessage, "ISM1StatusMessage", _process_ISM1StatusMessage}, + // SM devices + {EMS_MODEL_SM, EMS_TYPE_SM10Monitor, "SM10Monitor", _process_SM10Monitor}, + {EMS_MODEL_SM, EMS_TYPE_SM100Monitor, "SM100Monitor", _process_SM100Monitor}, + {EMS_MODEL_SM, EMS_TYPE_SM100Status, "SM100Status", _process_SM100Status}, + {EMS_MODEL_SM, EMS_TYPE_SM100Status2, "SM100Status2", _process_SM100Status2}, + {EMS_MODEL_SM, EMS_TYPE_SM100Energy, "SM100Energy", _process_SM100Energy}, + {EMS_MODEL_SM, EMS_TYPE_ISM1StatusMessage, "ISM1StatusMessage", _process_ISM1StatusMessage}, + {EMS_MODEL_SM, EMS_TYPE_ISM1Set, "ISM1Set", _process_ISM1Set}, + + // heatpunps + {EMS_MODEL_HP, EMS_TYPE_HPMonitor1, "HeatPumpMonitor1", _process_HPMonitor1}, + {EMS_MODEL_HP, EMS_TYPE_HPMonitor2, "HeatPumpMonitor2", _process_HPMonitor2}, // RC10 {EMS_MODEL_RC10, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, @@ -182,15 +186,19 @@ const _EMS_Type EMS_Types[] = { }; // 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 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 +uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types +uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of boiler models +uint8_t _Solar_Module_Types_max = ArraySize(SolarModule_Types); // number of solar module types +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 +uint8_t _HeatPump_Types_max = ArraySize(HeatPump_Types); // number of defined heatpuimp types -// these structs contain the data we store from the Boiler and Thermostat -_EMS_Boiler EMS_Boiler; // for boiler -_EMS_Thermostat EMS_Thermostat; // for thermostat -_EMS_Other EMS_Other; // for other known EMS devices +// these structs contain the data we store from the specific EMS devices +_EMS_Boiler EMS_Boiler; // for boiler +_EMS_Thermostat EMS_Thermostat; // for thermostat +_EMS_SolarModule EMS_SolarModule; // for solar modules +_EMS_HeatPump EMS_HeatPump; // for heatpumps +_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, 0x24, 0x26, @@ -212,7 +220,6 @@ const uint32_t EMS_BUS_TIMEOUT = 15000; // timeout in ms before recogni const uint32_t EMS_POLL_TIMEOUT = 5000000; // timeout in microseconds 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) void ems_init() { // overall status EMS_Sys_Status.emsRxPgks = 0; @@ -229,6 +236,7 @@ void ems_init() { EMS_Sys_Status.emsPollFrequency = 0; EMS_Sys_Status.txRetryCount = 0; EMS_Sys_Status.emsReverse = false; + EMS_Sys_Status.emsTxMode = 0; // thermostat EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_SHORT_NOTSET; @@ -239,8 +247,8 @@ void ems_init() { EMS_Thermostat.day = 0; EMS_Thermostat.month = 0; EMS_Thermostat.year = 0; - EMS_Thermostat.mode = 255; // dummy value - EMS_Thermostat.day_mode = 255; // dummy value + EMS_Thermostat.mode = EMS_VALUE_INT_NOTSET; + EMS_Thermostat.day_mode = EMS_VALUE_INT_NOTSET; EMS_Thermostat.device_id = EMS_ID_NONE; EMS_Thermostat.write_supported = false; EMS_Thermostat.hc = 1; // default heating circuit is 1 @@ -259,35 +267,35 @@ void ems_init() { EMS_Boiler.wWComfort = EMS_VALUE_INT_NOTSET; // UBAMonitorFast - EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow 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 - EMS_Boiler.heatPmp = EMS_VALUE_INT_NOTSET; // Boiler pump on/off - EMS_Boiler.wWHeat = EMS_VALUE_INT_NOTSET; // 3-way valve on WW - 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_SHORT_NOTSET; // Flame current in micro amps - EMS_Boiler.sysPress = EMS_VALUE_INT_NOTSET; // System pressure + EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow temperature + EMS_Boiler.curFlowTemp = EMS_VALUE_USHORT_NOTSET; // Current flow temperature + EMS_Boiler.retTemp = EMS_VALUE_USHORT_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 + EMS_Boiler.heatPmp = EMS_VALUE_INT_NOTSET; // Boiler pump on/off + EMS_Boiler.wWHeat = EMS_VALUE_INT_NOTSET; // 3-way valve on WW + 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_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_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 + EMS_Boiler.extTemp = EMS_VALUE_SHORT_NOTSET; // Outside temperature + EMS_Boiler.boilTemp = EMS_VALUE_USHORT_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_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 + EMS_Boiler.wWCurTmp = EMS_VALUE_USHORT_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 EMS_Boiler.wWCurFlow = EMS_VALUE_INT_NOTSET; // UBATotalUptimeMessage @@ -298,16 +306,26 @@ 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 + // Solar Module values + EMS_SolarModule.collectorTemp = EMS_VALUE_SHORT_NOTSET; // collector temp from SM10/SM100 + EMS_SolarModule.bottomTemp = EMS_VALUE_SHORT_NOTSET; // bottom temp from SM10/SM100 + EMS_SolarModule.pumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10/SM100 + EMS_SolarModule.pump = EMS_VALUE_INT_NOTSET; // pump active + EMS_SolarModule.EnergyLastHour = EMS_VALUE_USHORT_NOTSET; + EMS_SolarModule.EnergyToday = EMS_VALUE_USHORT_NOTSET; + EMS_SolarModule.EnergyTotal = EMS_VALUE_USHORT_NOTSET; + EMS_SolarModule.device_id = EMS_ID_NONE; + EMS_SolarModule.model_id = EMS_MODEL_NONE; + EMS_SolarModule.product_id = EMS_ID_NONE; + EMS_SolarModule.pumpWorkMin = EMS_VALUE_LONG_NOTSET; + EMS_SolarModule.setpoint_maxBottomTemp = EMS_VALUE_SHORT_NOTSET; + // Other EMS devices values - EMS_Other.SMcollectorTemp = EMS_VALUE_SHORT_NOTSET; // collector temp from SM10/SM100 - EMS_Other.SMbottomTemp = EMS_VALUE_SHORT_NOTSET; // bottom temp from SM10/SM100 - EMS_Other.SMpumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10/SM100 - EMS_Other.SMpump = EMS_VALUE_INT_NOTSET; // pump active - EMS_Other.SMEnergyLastHour = EMS_VALUE_SHORT_NOTSET; - EMS_Other.SMEnergyToday = EMS_VALUE_SHORT_NOTSET; - EMS_Other.SMEnergyTotal = EMS_VALUE_SHORT_NOTSET; - EMS_Other.HPModulation = EMS_VALUE_INT_NOTSET; - EMS_Other.HPSpeed = EMS_VALUE_INT_NOTSET; + EMS_HeatPump.HPModulation = EMS_VALUE_INT_NOTSET; + EMS_HeatPump.HPSpeed = EMS_VALUE_INT_NOTSET; + EMS_HeatPump.device_id = EMS_ID_NONE; + EMS_HeatPump.model_id = EMS_MODEL_NONE; + EMS_HeatPump.product_id = EMS_ID_NONE; // calculated values EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off @@ -322,9 +340,6 @@ void ems_init() { EMS_Thermostat.product_id = EMS_ID_NONE; strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version)); - // set other types - EMS_Other.SM = false; - EMS_Other.HP = false; // default logging is none ems_setLogging(EMS_SYS_LOGGING_DEFAULT); @@ -340,6 +355,23 @@ bool ems_getPoll() { return EMS_Sys_Status.emsPollEnabled; } +void ems_setTxMode(uint8_t mode) { + EMS_Sys_Status.emsTxMode = mode; + + // special case for Junkers. If tx_mode is 3 then set the reverse poll flag + // https://github.com/proddy/EMS-ESP/issues/103#issuecomment-495945850 + if (mode == 3) { + EMS_Sys_Status.emsReverse = true; + myDebug_P(PSTR("Forcing emsReverse for Junkers")); + } else { + EMS_Sys_Status.emsReverse = false; + } +} + +uint8_t ems_getTxMode() { + return EMS_Sys_Status.emsTxMode; +} + bool ems_getEmsRefreshed() { return EMS_Sys_Status.emsRefreshed; } @@ -360,14 +392,30 @@ bool ems_getThermostatEnabled() { return (EMS_Thermostat.device_id != EMS_ID_NONE); } +bool ems_getSolarModuleEnabled() { + return (EMS_SolarModule.device_id != EMS_ID_NONE); +} + +bool ems_getHeatPumpEnabled() { + return (EMS_HeatPump.device_id != EMS_ID_NONE); +} + uint8_t ems_getThermostatModel() { return (EMS_Thermostat.model_id); } +uint8_t ems_getSolarModuleModel() { + return (EMS_SolarModule.model_id); +} + void ems_setTxDisabled(bool b) { EMS_Sys_Status.emsTxDisabled = b; } +bool ems_getTxDisabled() { + return (EMS_Sys_Status.emsTxDisabled); +} + uint32_t ems_getPollFrequency() { return EMS_Sys_Status.emsPollFrequency; } @@ -401,6 +449,8 @@ void ems_setLogging(_EMS_SYS_LOGGING loglevel) { myDebug_P(PSTR("System Logging set to Verbose")); } else if (loglevel == EMS_SYS_LOGGING_THERMOSTAT) { myDebug_P(PSTR("System Logging set to Thermostat only")); + } else if (loglevel == EMS_SYS_LOGGING_SOLARMODULE) { + myDebug_P(PSTR("System Logging set to Solar Module only")); } else if (loglevel == EMS_SYS_LOGGING_RAW) { myDebug_P(PSTR("System Logging set to Raw mode")); } @@ -457,7 +507,7 @@ char * _smallitoa3(uint16_t value, char * 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) { +int _ems_findType(uint16_t type) { uint8_t i = 0; bool typeFound = false; // scan through known ID types @@ -498,7 +548,7 @@ void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, strlcat(output_str, prefix, sizeof(output_str)); if (!raw) { - strlcat(output_str, " telegram: ", sizeof(output_str)); + strlcat(output_str, "telegram: ", sizeof(output_str)); } for (int i = 0; i < (length - 1); i++) { @@ -580,19 +630,27 @@ void _ems_sendTelegram() { // for a READ or VALIDATE EMS_TxTelegram.data[1] = EMS_TxTelegram.dest | 0x80; // read has 8th bit set } - EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type - EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; // offset - // see if it has data, add the single data value byte - // otherwise leave it alone and assume the data has been pre-populated - if (EMS_TxTelegram.length == EMS_MIN_TELEGRAM_LENGTH) { - // for reading this is #bytes we want to read (the size) - // for writing its the value we want to write - EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; + // complete the rest of the header depending on EMS or EMS+ + if (EMS_TxTelegram.type > 0xFF) { + // EMS 2.0 / emsplus + EMS_TxTelegram.data[2] = 0xFF; // fixed value indicating an extended message + EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; + EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; // for read its #bytes to return, for write it the value to set + EMS_TxTelegram.data[5] = EMS_TxTelegram.type >> 8; // type, 1st byte + EMS_TxTelegram.data[6] = EMS_TxTelegram.type & 0xFF; // type, 2nd byte + EMS_TxTelegram.length += 2; // add 2 bytes to length to compensate the extra FF and byte for the type + } else { + // EMS 1.0 + EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type + EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; // offset + if (EMS_TxTelegram.length == EMS_MIN_TELEGRAM_LENGTH) { + EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; // for read its #bytes to return, for write it the value to set + } } + // finally calculate CRC and add it to the end - uint8_t crc = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); - EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = crc; + EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // print debug info if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { @@ -669,17 +727,27 @@ void _createValidate() { * When a telegram is processed we forcefully erase it from the stack to prevent overflow */ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { - // create the Rx package - static _EMS_RxTelegram EMS_RxTelegram; - static uint32_t _last_emsPollFrequency = 0; + static uint32_t _last_emsPollFrequency = 0; - EMS_RxTelegram.telegram = telegram; - EMS_RxTelegram.timestamp = millis(); - EMS_RxTelegram.length = 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 + * Roger Wilco: we have different types here: + * EMS_ID_ME && length == 1 && EMS_TX_STATUS_IDLE && EMS_RX_STATUS_IDLE: polling request + * EMS_ID_ME && length > 1 && EMS_TX_STATUS_IDLE && EMS_RX_STATUS_IDLE: direct telegram + * (EMS_TX_SUCCESS || EMS_TX_ERROR) && EMS_TX_STATUS_WAIT: response, free the EMS bus + * + * In addition, it may happen that we where interrupted (f.e. by WIFI activity) and the + * buffer isn't valid anymore, so we must not answer at all... + */ + if (EMS_Sys_Status.emsRxStatus != EMS_RX_STATUS_IDLE) { + if (EMS_Sys_Status.emsLogging > EMS_SYS_LOGGING_NONE) { + myDebug_P(PSTR("** [DEBUG MODE] We missed the bus - Rx non-idle!")); //TODO tidy up error logging + } + return; + } - // 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 @@ -728,7 +796,11 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { return; } - // fill in the rest of the telegram + static _EMS_RxTelegram EMS_RxTelegram; // create the Rx package + EMS_RxTelegram.telegram = telegram; + EMS_RxTelegram.timestamp = millis(); + EMS_RxTelegram.length = length; + EMS_RxTelegram.src = telegram[0] & 0x7F; // removing 8th bit as we deal with both reads and writes here EMS_RxTelegram.dest = telegram[1] & 0x7F; // remove 8th bit (don't care if read or write) EMS_RxTelegram.offset = telegram[3]; // offset is always 4th byte @@ -766,9 +838,16 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { EMS_RxTelegram.data_length = length - 5; // remove 4 bytes header plus CRC } + // if we are in raw logging mode then just print out the telegram as it is + // but still continue to process it + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW) { + _debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true); + } + // Assume at this point we have something that vaguely resembles a telegram in the format [src] [dest] [type] [offset] [data] [crc] // validate the CRC, if it's bad ignore it if (telegram[length - 1] != _crcCalculator(telegram, length)) { + LA_PULSE(200); EMS_Sys_Status.emxCrcErr++; if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { _debugPrintTelegram("Corrupt telegram: ", &EMS_RxTelegram, COLOR_RED, true); @@ -776,12 +855,6 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { return; } - // if we are in raw logging mode then just print out the telegram as it is - // but still continue to process it - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW) { - _debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true); - } - // 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 = EMS_RxTelegram.timestamp; // timestamp of last read @@ -796,9 +869,10 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { */ void _printMessage(_EMS_RxTelegram * EMS_RxTelegram) { // header info - uint8_t src = EMS_RxTelegram->src; - uint8_t dest = EMS_RxTelegram->dest; - uint16_t type = EMS_RxTelegram->type; + uint8_t src = EMS_RxTelegram->src; + uint8_t dest = EMS_RxTelegram->dest; + uint16_t type = EMS_RxTelegram->type; + uint8_t length = EMS_RxTelegram->data_length; char output_str[200] = {0}; char buffer[16] = {0}; @@ -850,27 +924,38 @@ void _printMessage(_EMS_RxTelegram * EMS_RxTelegram) { strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); } - // type - strlcat(output_str, ", type 0x", sizeof(output_str)); + if (length != 0) { + // type + strlcat(output_str, ", type 0x", sizeof(output_str)); - if (EMS_RxTelegram->emsplus) { - strlcat(output_str, _hextoa(type >> 8, buffer), sizeof(output_str)); - strlcat(output_str, _hextoa(type & 0xFF, buffer), sizeof(output_str)); - } else { - strlcat(output_str, _hextoa(type, buffer), sizeof(output_str)); + if (EMS_RxTelegram->emsplus) { + strlcat(output_str, _hextoa(type >> 8, buffer), sizeof(output_str)); + strlcat(output_str, _hextoa(type & 0xFF, buffer), sizeof(output_str)); + } else { + strlcat(output_str, _hextoa(type, buffer), sizeof(output_str)); + } } + strlcat(output_str, ", ", sizeof(output_str)); + + 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.device_id) || (dest == EMS_Thermostat.device_id)) { _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); } + } else if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_SOLARMODULE) { + // only print ones to/from thermostat if logging is set to thermostat only + if ((src == EMS_SolarModule.device_id) || (dest == EMS_SolarModule.device_id)) { + _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); + } } else { // always print _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); } } + /** * print detailed telegram * and then call its callback if there is one defined @@ -909,7 +994,7 @@ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { // 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) { - if ((EMS_Types[i].processType_cb) != (void *)NULL) { + if ((EMS_Types[i].processType_cb) != nullptr) { // print non-verbose message if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) || (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE)) { myDebug_P(PSTR("<--- %s(0x%02X)"), EMS_Types[i].typeString, type); @@ -990,21 +1075,27 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { if (((EMS_RxTelegram->src & 0x7F) == (EMS_TxTelegram.dest & 0x7F)) && (EMS_RxTelegram->type == EMS_TxTelegram.type)) { // all checks out, read was successful, remove tx from queue and continue to process telegram _removeTxQueue(); - EMS_Sys_Status.emsRxPgks++; // increment counter + EMS_Sys_Status.emsRxPgks++; // increment Rx happy counter ems_setEmsRefreshed(EMS_TxTelegram.forceRefresh); // does mqtt need refreshing? } else { // read not OK, we didn't get back a telegram we expected - // leave on queue and try again, but continue to process what we received as it may be important - EMS_Sys_Status.txRetryCount++; - // if retried too many times, give up and remove it - if (EMS_Sys_Status.txRetryCount >= TX_WRITE_TIMEOUT_COUNT) { - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("Read failed. Giving up, removing from queue")); - } + + // first see if we got a response back from the sender saying its an unknown command + if (EMS_RxTelegram->data_length == 0) { _removeTxQueue(); } else { - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("Read failed. Retrying attempt %d/%d..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); + // leave on queue and try again, but continue to process what we received as it may be important + EMS_Sys_Status.txRetryCount++; + // if retried too many times, give up and remove it + if (EMS_Sys_Status.txRetryCount >= TX_WRITE_TIMEOUT_COUNT) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("Read failed. Giving up, removing from queue")); + } + _removeTxQueue(); + } else { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("Read failed. Retrying attempt %d/%d..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); + } } } } @@ -1013,7 +1104,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { // should not get here, since this is handled earlier receiving a 01 or 04 - myDebug_P(PSTR("** Error ! Write - should not be here")); + myDebug_P(PSTR("** Error! Write - should not be here")); } if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { @@ -1173,7 +1264,7 @@ void _process_UBAMonitorSlow(_EMS_RxTelegram * EMS_RxTelegram) { */ void _process_RC10StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_OFFSET_RC10StatusMessage_setpoint); // is * 2 - EMS_Thermostat.curr_roomTemp = _toByte(EMS_OFFSET_RC10StatusMessage_curr); // is * 10 + EMS_Thermostat.curr_roomTemp = _toShort(EMS_OFFSET_RC10StatusMessage_curr); // is * 10 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1247,9 +1338,7 @@ void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RCPLUSGet_mode_day, 1); // get day mode flag - // room night setpoint is _toByte(2) (value is *2) - // boiler set temp is _toByte(4) (value is *2) - // day night is byte(8), 0x01 for night, 0x00 for day + EMS_Thermostat.mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0) } // actual set point @@ -1263,6 +1352,13 @@ void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { if (EMS_RxTelegram->offset == 6) { // to add... } + + // thermostat mode auto/manual, examples: + // manual : 10 00 FF 0A 01 A5 02 (CRC=16) #data=1 + // auto : Thermostat -> all, type 0x01A5 telegram: 10 00 FF 0A 01 A5 03 (CRC=17) #data=1 + if (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSStatusMessage_mode) { + EMS_Thermostat.mode = _bitRead(0, 0); // bit 0 + } } /** @@ -1284,10 +1380,12 @@ void _process_RCPLUSStatusMode(_EMS_RxTelegram * EMS_RxTelegram) { * FR10 Junkers - type x6F01 */ void _process_JunkersStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - // e.g. for FR10: 90 00 FF 00 00 6F 03 01 00 BE 00 BF - // e.g. for FW100: 90 00 FF 00 00 6F 03 02 00 D7 00 DA F3 34 00 C4 - EMS_Thermostat.curr_roomTemp = _toShort(EMS_OFFSET_JunkersStatusMessage_curr); // value is * 10 - EMS_Thermostat.setpoint_roomTemp = _toShort(EMS_OFFSET_JunkersStatusMessage_setpoint); // value is * 10 + if (EMS_RxTelegram->offset == 0) { + // e.g. for FR10: 90 00 FF 00 00 6F 03 01 00 BE 00 BF + // e.g. for FW100: 90 00 FF 00 00 6F 03 02 00 D7 00 DA F3 34 00 C4 + EMS_Thermostat.curr_roomTemp = _toShort(EMS_OFFSET_JunkersStatusMessage_curr); // value is * 10 + EMS_Thermostat.setpoint_roomTemp = _toShort(EMS_OFFSET_JunkersStatusMessage_setpoint); // value is * 10 + } } /** @@ -1348,12 +1446,11 @@ void _process_RCOutdoorTempMessage(_EMS_RxTelegram * EMS_RxTelegram) { * SM10Monitor - type 0x97 */ void _process_SM10Monitor(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Other.SMcollectorTemp = _toShort(2); // collector temp from SM10, is *10 - EMS_Other.SMbottomTemp = _toShort(5); // bottom temp from SM10, is *10 - EMS_Other.SMpumpModulation = _toByte(4); // modulation solar pump - EMS_Other.SMpump = _bitRead(7, 1); // active if bit 1 is set + EMS_SolarModule.collectorTemp = _toShort(2); // collector temp from SM10, is *10 + EMS_SolarModule.bottomTemp = _toShort(5); // bottom temp from SM10, is *10 + EMS_SolarModule.pumpModulation = _toByte(4); // modulation solar pump + EMS_SolarModule.pump = _bitRead(7, 1); // active if bit 1 is set - EMS_Other.SM = true; EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1369,13 +1466,12 @@ void _process_SM100Monitor(_EMS_RxTelegram * EMS_RxTelegram) { return; } - EMS_Other.SMcollectorTemp = _toShort(0); // collector temp from SM100, is *10 + EMS_SolarModule.collectorTemp = _toShort(0); // collector temp from SM100, is *10 if (EMS_RxTelegram->data_length > 2) { - EMS_Other.SMbottomTemp = _toShort(2); // bottom temp from SM100, is *10 + EMS_SolarModule.bottomTemp = _toShort(2); // bottom temp from SM100, is *10 } - EMS_Other.SM = true; EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1387,13 +1483,12 @@ void _process_SM100Monitor(_EMS_RxTelegram * EMS_RxTelegram) { void _process_SM100Status(_EMS_RxTelegram * EMS_RxTelegram) { // check for complete telegram if (EMS_RxTelegram->offset == 0) { - EMS_Other.SMpumpModulation = _toByte(9); // modulation solar pump + EMS_SolarModule.pumpModulation = _toByte(9); // modulation solar pump } else if (EMS_RxTelegram->offset == 0x09) { // or short telegram with a single byte with offset 09 - EMS_Other.SMpumpModulation = _toByte(0); // modulation solar pump + EMS_SolarModule.pumpModulation = _toByte(0); // modulation solar pump } - EMS_Other.SM = true; EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1403,13 +1498,12 @@ void _process_SM100Status(_EMS_RxTelegram * EMS_RxTelegram) { void _process_SM100Status2(_EMS_RxTelegram * EMS_RxTelegram) { // check for complete telegram if (EMS_RxTelegram->offset == 0) { - EMS_Other.SMpump = _bitRead(10, 2); // 03=off 04=on at offset 10 which is byte 10 + EMS_SolarModule.pump = _bitRead(10, 2); // 03=off 04=on at offset 10 which is byte 10 } else if (EMS_RxTelegram->offset == 0x0A) { // or short telegram with a single byte with offset 0A - EMS_Other.SMpump = _bitRead(0, 2); // 03=off 04=on + EMS_SolarModule.pump = _bitRead(0, 2); // 03=off 04=on } - EMS_Other.SM = true; EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1418,11 +1512,10 @@ void _process_SM100Status2(_EMS_RxTelegram * EMS_RxTelegram) { * e.g. 30 00 FF 00 02 8E 00 00 00 00 00 00 06 C5 00 00 76 35 */ void _process_SM100Energy(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Other.SMEnergyLastHour = _toShort(2); // last hour / 10 in Wh - EMS_Other.SMEnergyToday = _toShort(6); // todays in Wh - EMS_Other.SMEnergyTotal = _toShort(10); // total / 10 in kWh + EMS_SolarModule.EnergyLastHour = _toShort(2); // last hour / 10 in Wh + EMS_SolarModule.EnergyToday = _toShort(6); // todays in Wh + EMS_SolarModule.EnergyTotal = _toShort(10); // total / 10 in kWh - EMS_Other.SM = true; EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1430,9 +1523,8 @@ void _process_SM100Energy(_EMS_RxTelegram * EMS_RxTelegram) { * Type 0xE3 - HeatPump Monitor 1 */ void _process_HPMonitor1(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Other.HPModulation = _toByte(14); // modulation % + EMS_HeatPump.HPModulation = _toByte(14); // modulation % - EMS_Other.HP = true; EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1440,9 +1532,8 @@ void _process_HPMonitor1(_EMS_RxTelegram * EMS_RxTelegram) { * Type 0xE5 - HeatPump Monitor 2 */ void _process_HPMonitor2(_EMS_RxTelegram * EMS_RxTelegram) { - EMS_Other.HPSpeed = _toByte(25); // speed % + EMS_HeatPump.HPSpeed = _toByte(25); // speed % - EMS_Other.HP = true; EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1450,10 +1541,31 @@ void _process_HPMonitor2(_EMS_RxTelegram * EMS_RxTelegram) { * Junkers ISM1 Solar Module - type 0x0003 EMS+ for energy readings */ void _process_ISM1StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - // e.g. B0 00 FF 00 00 03 32 00 00 00 00 13 00 D6 00 00 00 FB D0 F0 - EMS_Other.SMcollectorTemp = _toShort(4); // Collector Temperature - EMS_Other.SMbottomTemp = _toShort(6); // Temperature Bottom of Solar Boiler - EMS_Other.SM = true; + if (EMS_RxTelegram->offset == 0) { + // e.g. B0 00 FF 00 00 03 32 00 00 00 00 13 00 D6 00 00 00 FB D0 F0 + EMS_SolarModule.collectorTemp = _toShort(4); // Collector Temperature + EMS_SolarModule.bottomTemp = _toShort(6); // Temperature Bottom of Solar Boiler + EMS_SolarModule.EnergyLastHour = _toShort(2); // Solar Energy produced in last hour - is * 10 and handled in ems-esp.cpp + EMS_SolarModule.pump = _bitRead(8, 0); // Solar pump on (1) or off (0) + EMS_SolarModule.pumpWorkMin = _toLong(10); + } + + if (EMS_RxTelegram->offset == 4) { + // e.g. B0 00 FF 04 00 03 02 E5 + EMS_SolarModule.collectorTemp = _toShort(0); // Collector Temperature + } +} + + +/* + * Junkers ISM1 Solar Module - type 0x0001 EMS+ for setting values + */ +void _process_ISM1Set(_EMS_RxTelegram * EMS_RxTelegram) { + if (EMS_RxTelegram->offset == 6) { + // e.g. 90 30 FF 06 00 01 50 (CRC=2C) + // to implement: change max solar boiler temperature + EMS_SolarModule.setpoint_maxBottomTemp = _toByte(0); + } } /** @@ -1508,7 +1620,6 @@ void ems_clearDeviceList() { */ void _addDevice(uint8_t product_id, uint8_t device_id, char * version, const char * model_string) { _Generic_Type device; - // if its a duplicate don't add bool found = false; for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) { @@ -1516,7 +1627,6 @@ void _addDevice(uint8_t product_id, uint8_t device_id, char * version, const cha found = true; } } - if (!found) { device.product_id = product_id; device.device_id = device_id; @@ -1631,6 +1741,64 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { return; } + + // look for Solar Modules + i = 0; + while (i < _Solar_Module_Types_max) { + if (SolarModule_Types[i].product_id == product_id) { + typeFound = true; // we have a matching product id. i is the index. + break; + } + i++; + } + + if (typeFound) { + myDebug_P(PSTR("Solar Module found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), + SolarModule_Types[i].model_string, + SolarModule_Types[i].device_id, + product_id, + version); + + // add to list + _addDevice(product_id, SolarModule_Types[i].device_id, version, SolarModule_Types[i].model_string); + + myDebug_P(PSTR("Solar Module support enabled.")); + EMS_SolarModule.device_id = SolarModule_Types[i].device_id; + EMS_SolarModule.product_id = product_id; + strlcpy(EMS_SolarModule.version, version, sizeof(EMS_SolarModule.version)); + + // fetch Solar Module values + ems_getSolarModuleValues(); + return; + } + + // look for heatpumps + i = 0; + while (i < _HeatPump_Types_max) { + if (HeatPump_Types[i].product_id == product_id) { + typeFound = true; // we have a matching product id. i is the index. + break; + } + i++; + } + + if (typeFound) { + myDebug_P(PSTR("Heat Pump found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), + HeatPump_Types[i].model_string, + HeatPump_Types[i].device_id, + product_id, + version); + + // add to list + _addDevice(product_id, HeatPump_Types[i].device_id, version, HeatPump_Types[i].model_string); + + myDebug_P(PSTR("Heat Pump support enabled.")); + EMS_HeatPump.device_id = SolarModule_Types[i].device_id; + EMS_HeatPump.product_id = product_id; + strlcpy(EMS_HeatPump.version, version, sizeof(EMS_HeatPump.version)); + return; + } + // finally look for the other EMS devices i = 0; while (i < _Other_Types_max) { @@ -1646,26 +1814,9 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { // add to list _addDevice(product_id, Other_Types[i].device_id, version, Other_Types[i].model_string); - - // see if this is a Solar Module SM10 - if (Other_Types[i].device_id == EMS_ID_SM) { - EMS_Other.SM = true; // we have detected a SM10 - myDebug_P(PSTR("SM10 Solar Module support enabled.")); - } - - // see if this is a HeatPump - if (Other_Types[i].device_id == EMS_ID_HP) { - EMS_Other.HP = true; // we have detected a HP - myDebug_P(PSTR("HeatPump support enabled.")); - } - - // fetch other values - ems_getOtherValues(); return; - } else { myDebug_P(PSTR("Unrecognized device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), EMS_RxTelegram->src, product_id, version); - // add to list _addDevice(product_id, EMS_RxTelegram->src, version, "unknown?"); } @@ -1774,22 +1925,32 @@ void ems_getThermostatValues() { uint8_t type = EMS_Thermostat.device_id; uint8_t hc = EMS_Thermostat.hc; - if (model_id == EMS_MODEL_RC20) { - ems_doReadCommand(EMS_TYPE_RC20StatusMessage, type); // to get the setpoint temp + switch (model_id) { + case EMS_MODEL_RC20: + ems_doReadCommand(EMS_TYPE_RC20StatusMessage, type); // to get the temps ems_doReadCommand(EMS_TYPE_RC20Set, type); // to get the mode - } else if (model_id == EMS_MODEL_RC30) { - ems_doReadCommand(EMS_TYPE_RC30StatusMessage, type); // to get the setpoint temp + break; + case EMS_MODEL_RC30: + ems_doReadCommand(EMS_TYPE_RC30StatusMessage, type); // to get the temps ems_doReadCommand(EMS_TYPE_RC30Set, type); // to get the mode - } else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) { + break; + case EMS_MODEL_EASY: + ems_doReadCommand(EMS_TYPE_EasyStatusMessage, type); + break; + case EMS_MODEL_RC35: + case EMS_MODEL_ES73: if (hc == 1) { - ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC1, type); // to get the setpoint temp + ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC1, type); // to get the temps ems_doReadCommand(EMS_TYPE_RC35Set_HC1, type); // to get the mode } else if (hc == 2) { - ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC2, type); // to get the setpoint temp + ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC2, type); // to get the temps ems_doReadCommand(EMS_TYPE_RC35Set_HC2, type); // to get the mode } - } else if ((model_id == EMS_MODEL_EASY)) { - ems_doReadCommand(EMS_TYPE_EasyStatusMessage, type); + break; + case EMS_MODEL_RC300: + ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage, type); + default: + break; } ems_doReadCommand(EMS_TYPE_RCTime, type); // get Thermostat time @@ -1809,14 +1970,21 @@ void ems_getBoilerValues() { /* * Get other values from EMS devices */ -void ems_getOtherValues() { - if (EMS_Other.SM) { - ems_doReadCommand(EMS_TYPE_SM10Monitor, EMS_ID_SM); // fetch all from SM10Monitor +void ems_getSolarModuleValues() { + uint8_t product_id = EMS_SolarModule.product_id; + + if (ems_getSolarModuleEnabled()) { + if (product_id == EMS_PRODUCTID_SM10) { + ems_doReadCommand(EMS_TYPE_SM10Monitor, EMS_ID_SM); // fetch all from SM10Monitor + } else if (product_id == EMS_PRODUCTID_SM100) { + ems_doReadCommand(EMS_TYPE_SM100Monitor, EMS_ID_SM); // fetch all from SM100Monitor + } } } /** - * returns current thermostat type as a string + * returns current thermostat type as a string + * by looking up the product_id */ char * ems_getThermostatDescription(char * buffer) { uint8_t size = 128; @@ -1898,6 +2066,88 @@ char * ems_getBoilerDescription(char * buffer) { return buffer; } +/** + * returns current Solar Module type as a string + */ +char * ems_getSolarModuleDescription(char * buffer) { + uint8_t size = 128; + if (!ems_getSolarModuleEnabled()) { + strlcpy(buffer, "", size); + } else { + int i = 0; + bool found = false; + char tmp[6] = {0}; + + // scan through known ID types + while (i < _Solar_Module_Types_max) { + if (SolarModule_Types[i].product_id == EMS_SolarModule.product_id) { + found = true; // we have a match + break; + } + i++; + } + if (found) { + strlcpy(buffer, SolarModule_Types[i].model_string, size); + } else { + strlcpy(buffer, "DeviceID: 0x", size); + strlcat(buffer, _hextoa(EMS_SolarModule.device_id, tmp), size); + } + + strlcat(buffer, " (ProductID:", size); + if (EMS_SolarModule.product_id == EMS_ID_NONE) { + strlcat(buffer, "?", size); + } else { + strlcat(buffer, itoa(EMS_SolarModule.product_id, tmp, 10), size); + } + strlcat(buffer, " Version:", size); + strlcat(buffer, EMS_SolarModule.version, size); + strlcat(buffer, ")", size); + } + + return buffer; +} + +/** + * returns current Heat Pump type as a string + */ +char * ems_getHeatPumpDescription(char * buffer) { + uint8_t size = 128; + if (!ems_getHeatPumpEnabled()) { + strlcpy(buffer, "", size); + } else { + int i = 0; + bool found = false; + char tmp[6] = {0}; + + // scan through known ID types + while (i < _HeatPump_Types_max) { + if (HeatPump_Types[i].product_id == EMS_HeatPump.product_id) { + found = true; // we have a match + break; + } + i++; + } + if (found) { + strlcpy(buffer, HeatPump_Types[i].model_string, size); + } else { + strlcpy(buffer, "DeviceID: 0x", size); + strlcat(buffer, _hextoa(EMS_HeatPump.device_id, tmp), size); + } + + strlcat(buffer, " (ProductID:", size); + if (EMS_HeatPump.product_id == EMS_ID_NONE) { + strlcat(buffer, "?", size); + } else { + strlcat(buffer, itoa(EMS_HeatPump.product_id, tmp, 10), size); + } + strlcat(buffer, " Version:", size); + strlcat(buffer, EMS_HeatPump.version, size); + strlcat(buffer, ")", size); + } + + return buffer; +} + /** * Find the versions of our connected devices */ @@ -1914,6 +2164,11 @@ void ems_scanDevices() { Device_Ids.push_back(tt.device_id); } + // copy over solar modules + for (_SolarModule_Type sm : SolarModule_Types) { + Device_Ids.push_back(sm.device_id); + } + // copy over others for (_Other_Type ot : Other_Types) { Device_Ids.push_back(ot.device_id); @@ -1949,6 +2204,16 @@ void ems_printAllDevices() { Boiler_Types[i].product_id); } + myDebug_P(PSTR("\nThese %d devices are supported as solar module devices:"), _Solar_Module_Types_max); + for (i = 0; i < _Solar_Module_Types_max; i++) { + myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"), + COLOR_BOLD_ON, + SolarModule_Types[i].model_string, + COLOR_BOLD_OFF, + SolarModule_Types[i].device_id, + SolarModule_Types[i].product_id); + } + myDebug_P(PSTR("\nThese %d devices are supported as other known EMS devices:"), _Other_Types_max); for (i = 0; i < _Other_Types_max; i++) { myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"), @@ -2004,6 +2269,23 @@ void ems_printDevices() { } } +/* + * prints the device list to a string for html parsing + */ +uint8_t ems_printDevices_s(char * buffer, uint16_t len) { + if (Devices.size() == 0) { + return 0; + } + + char s[100]; + for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) { + sprintf(s, "%s (DeviceID:0x%02X ProductID:%d Version:%s)
", (it)->model_string, (it)->device_id, (it)->product_id, (it)->version); + strlcat(buffer, s, len); + } + + return Devices.size(); +} + /** * Send a command to UART Tx to Read from another device * Read commands when sent must respond by the destination (target) immediately (or within 10ms) @@ -2036,17 +2318,17 @@ void ems_doReadCommand(uint16_t type, uint8_t dest, bool forceRefresh) { myDebug_P(PSTR("Requesting type %s(0x%02X) from dest 0x%02X"), EMS_Types[i].typeString, type, dest); } } - EMS_TxTelegram.action = EMS_TX_TELEGRAM_READ; // read command - EMS_TxTelegram.dest = dest; // 8th bit will be set to indicate a read - EMS_TxTelegram.offset = 0; // 0 for all data - EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; // is always 6 bytes long (including CRC at end) + EMS_TxTelegram.action = EMS_TX_TELEGRAM_READ; // read command + EMS_TxTelegram.dest = dest; // 8th bit will be set to indicate a read + EMS_TxTelegram.offset = 0; // 0 for all data EMS_TxTelegram.type = type; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; // EMS 1.0: 6 bytes long (including CRC at end), EMS+ will add 2 bytes. includes CRC EMS_TxTelegram.dataValue = EMS_MAX_TELEGRAM_LENGTH; // for a read this is the # bytes we want back EMS_TxTelegram.type_validate = EMS_ID_NONE; EMS_TxTelegram.comparisonValue = 0; EMS_TxTelegram.comparisonOffset = 0; EMS_TxTelegram.comparisonPostRead = EMS_ID_NONE; - EMS_TxTelegram.forceRefresh = forceRefresh; // should we send to MQTT after a successful read? + EMS_TxTelegram.forceRefresh = forceRefresh; // send to MQTT after a successful read EMS_TxQueue.push(EMS_TxTelegram); } @@ -2075,7 +2357,7 @@ void ems_sendRawTelegram(char * telegram) { } // and interate until end while (p != 0) { - if ((p = strtok(NULL, " ,"))) { + if ((p = strtok(nullptr, " ,"))) { strlcpy(value, p, sizeof(value)); uint8_t val = (uint8_t)strtol(value, 0, 16); EMS_TxTelegram.data[++count] = val; @@ -2454,7 +2736,7 @@ void ems_testTelegram(uint8_t test_num) { // and interate until end while (p != 0) { - if ((p = strtok(NULL, " ,"))) { + if ((p = strtok(nullptr, " ,"))) { strlcpy(value, p, sizeof(value)); uint8_t val = (uint8_t)strtol(value, 0, 16); telegram[++length] = val; @@ -2469,6 +2751,6 @@ void ems_testTelegram(uint8_t test_num) { // go an parse it ems_parseTelegram(telegram, length + 1); // include CRC in length #else - myDebug_P(PSTR("Firmware not compiled with test data set")); + myDebug_P(PSTR("Firmware not compiled with test data. Use -DTESTS")); #endif } diff --git a/src/ems.h b/src/ems.h index e20ca7660..800577200 100644 --- a/src/ems.h +++ b/src/ems.h @@ -12,27 +12,90 @@ #include +/* debug helper for logic analyzer + * create marker puls on GPIOx + * ° for Rx, we use GPIO14 + * ° for Tx, we use GPIO12 + */ +// clang-format off +#ifdef LOGICANALYZER +#define RX_MARK_PIN 14 +#define TX_MARK_PIN 12 + +#define RX_MARK_MASK (1 << RX_MARK_PIN) +#define TX_MARK_MASK (1 << TX_MARK_PIN) +#define MARKERS_MASK (RX_MARK_PIN | TX_MARK_PIN) + +#define GPIO_H(mask) (GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, (mask))) +#define GPIO_L(mask) (GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, (mask))) + +#define RX_PULSE(pulse) \ + do { \ + GPIO_H(RX_MARK_MASK); \ + delayMicroseconds(pulse); \ + GPIO_L(RX_MARK_MASK); \ + } while (0) +#define TX_PULSE(pulse) \ + do { \ + GPIO_H(TX_MARK_MASK); \ + delayMicroseconds(pulse); \ + GPIO_L(TX_MARK_MASK); \ + } while (0) +#define LA_PULSE(pulse) \ + do { \ + GPIO_H(MARKERS_MASK); \ + delayMicroseconds(pulse); \ + GPIO_L(MARKERS_MASK); \ + } while (0) + +#define INIT_MARKERS(void) \ + do { \ + pinMode(RX_MARK_PIN, OUTPUT); \ + pinMode(TX_MARK_PIN, OUTPUT); \ + GPIO_L(MARKERS_MASK); \ + } while (0) +#else +#define RX_PULSE(pulse) \ + {} +#define TX_PULSE(pulse) \ + {} +#define LA_PULSE(pulse) \ + {} +#define INIT_MARKERS(void) \ + {} +#define RX_MARK_MASK +#define TX_MARK_MASK +#define GPIO_H(mask) +#define GPIO_L(mask) +#endif +// clang-format on + #define EMS_ID_NONE 0x00 // used as a dest in broadcast messages and empty device IDs // Fixed EMS IDs #define EMS_ID_ME 0x0B // our device, hardcoded as the "Service Key" #define EMS_ID_BOILER 0x08 // all UBA Boilers have 0x08 -#define EMS_ID_SM 0x30 // Solar Module SM10 and SM100 +#define EMS_ID_SM 0x30 // Solar Module SM10, SM100 and ISM1 #define EMS_ID_HP 0x38 // HeatPump #define EMS_ID_GATEWAY 0x48 // KM200 Web Gateway #define EMS_PRODUCTID_HEATRONICS 95 // ProductID for a Junkers Heatronic3 device +#define EMS_PRODUCTID_SM10 73 // ProductID for SM10 solar module +#define EMS_PRODUCTID_SM100 163 // ProductID for SM10 solar module +#define EMS_PRODUCTID_ISM1 101 // ProductID for SM10 solar module + #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 32 -// default values +// default values for null values #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 signed shorts +#define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit unsigned ints/bytes +#define EMS_VALUE_SHORT_NOTSET -32768 // for 2-byte signed shorts +#define EMS_VALUE_USHORT_NOTSET 0x8000 // for 2-byte unsigned shorts #define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs #define EMS_THERMOSTAT_WRITE_YES true @@ -57,8 +120,11 @@ typedef enum { } _EMS_RX_STATUS; typedef enum { + EMS_TX_STATUS_OK, EMS_TX_STATUS_IDLE, // ready - EMS_TX_STATUS_WAIT // waiting for response from last Tx + EMS_TX_STATUS_WAIT, // waiting for response from last Tx + EMS_TX_WTD_TIMEOUT, // watchdog timeout during send + EMS_TX_BRK_DETECT // incoming BRK during Tx } _EMS_TX_STATUS; #define EMS_TX_SUCCESS 0x01 // EMS single byte after a Tx Write indicating a success @@ -74,11 +140,12 @@ typedef enum { /* EMS logging */ typedef enum { - EMS_SYS_LOGGING_NONE, // no messages - EMS_SYS_LOGGING_RAW, // raw data mode - EMS_SYS_LOGGING_BASIC, // only basic read/write messages - EMS_SYS_LOGGING_THERMOSTAT, // only telegrams sent from thermostat - EMS_SYS_LOGGING_VERBOSE // everything + EMS_SYS_LOGGING_NONE, // no messages + EMS_SYS_LOGGING_RAW, // raw data mode + EMS_SYS_LOGGING_BASIC, // only basic read/write messages + EMS_SYS_LOGGING_THERMOSTAT, // only telegrams sent from thermostat + EMS_SYS_LOGGING_SOLARMODULE, // only telegrams sent from thermostat + EMS_SYS_LOGGING_VERBOSE // everything } _EMS_SYS_LOGGING; // status/counters since last power on @@ -98,6 +165,7 @@ typedef struct { bool emsTxDisabled; // true to prevent all Tx uint8_t txRetryCount; // # times the last Tx was re-sent bool emsReverse; // if true, poll logic is reversed + uint8_t emsTxMode; // handles Tx logic } _EMS_Sys_Status; // The Tx send package @@ -148,20 +216,30 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { {0x00} // data }; +// where defintions are stored typedef struct { - uint8_t model_id; uint8_t product_id; char model_string[50]; } _Boiler_Type; typedef struct { - uint8_t model_id; + uint8_t product_id; + uint8_t device_id; + char model_string[50]; +} _SolarModule_Type; + +typedef struct { uint8_t product_id; uint8_t device_id; char model_string[50]; } _Other_Type; -// Definition for thermostat devices +typedef struct { + uint8_t product_id; + uint8_t device_id; + char model_string[50]; +} _HeatPump_Type; + typedef struct { uint8_t model_id; uint8_t product_id; @@ -170,6 +248,7 @@ typedef struct { bool write_supported; } _Thermostat_Type; +// for consolidating all types typedef struct { uint8_t product_id; uint8_t device_id; @@ -189,8 +268,8 @@ typedef struct { // UBAParameterWW // UBAMonitorFast uint8_t selFlowTemp; // Selected flow temperature - int16_t curFlowTemp; // Current flow temperature - int16_t retTemp; // Return temperature + uint16_t curFlowTemp; // Current flow temperature + uint16_t retTemp; // Return temperature uint8_t burnGas; // Gas on/off uint8_t fanWork; // Fan on/off uint8_t ignWork; // Ignition on/off @@ -206,14 +285,14 @@ typedef struct { // UBAParameterWW // UBAMonitorSlow int16_t extTemp; // Outside temperature - int16_t boilTemp; // Boiler temperature + uint16_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: + uint16_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 @@ -241,21 +320,39 @@ typedef struct { // UBAParameterWW * Telegram package defintions for Other EMS devices */ -// SM Solar Module - SM10Monitor/SM100Monitor typedef struct { - bool SM; // set true if there is a Solar Module available - bool HP; // set true if there is a Heat Pump available - int16_t SMcollectorTemp; // collector temp - int16_t SMbottomTemp; // bottom temp - uint8_t SMpumpModulation; // modulation solar pump - uint8_t SMpump; // pump active - int16_t SMEnergyLastHour; - int16_t SMEnergyToday; - int16_t SMEnergyTotal; uint8_t HPModulation; // heatpump modulation in % uint8_t HPSpeed; // speed 0-100 % + uint8_t device_id; // the device ID of the Heat Pump (e.g. 0x30) + uint8_t model_id; // Solar Module / Heat Pump model (e.g. 3 > EMS_MODEL_OTHER ) + uint8_t product_id; + char version[10]; +} _EMS_HeatPump; + +typedef struct { + uint8_t device_id; + uint8_t model_id; + uint8_t product_id; + char version[10]; } _EMS_Other; +// SM Solar Module - SM10/SM100/ISM1 +typedef struct { + int16_t collectorTemp; // collector temp + int16_t bottomTemp; // bottom temp + uint8_t pumpModulation; // modulation solar pump + uint8_t pump; // pump active + int16_t setpoint_maxBottomTemp; // setpoint for maximum collector temp + uint16_t EnergyLastHour; + uint16_t EnergyToday; + uint16_t EnergyTotal; + uint32_t pumpWorkMin; // Total solar pump operating time + uint8_t device_id; // the device ID of the Solar Module + uint8_t model_id; // Solar Module + uint8_t product_id; + char version[10]; +} _EMS_SolarModule; + // Thermostat data typedef struct { uint8_t device_id; // the device ID of the thermostat @@ -300,39 +397,48 @@ void ems_sendRawTelegram(char * telegram); void ems_scanDevices(); void ems_printAllDevices(); void ems_printDevices(); +uint8_t ems_printDevices_s(char * buffer, uint16_t len); void ems_printTxQueue(); void ems_testTelegram(uint8_t test_num); void ems_startupTelegrams(); bool ems_checkEMSBUSAlive(); void ems_clearDeviceList(); +void ems_setTxMode(uint8_t mode); -void ems_setThermostatTemp(float temperature, uint8_t temptype = 0); -void ems_setThermostatMode(uint8_t mode); -void ems_setThermostatHC(uint8_t hc); -void ems_setWarmWaterTemp(uint8_t temperature); -void ems_setFlowTemp(uint8_t temperature); -void ems_setWarmWaterActivated(bool activated); -void ems_setWarmTapWaterActivated(bool activated); -void ems_setPoll(bool b); -void ems_setLogging(_EMS_SYS_LOGGING loglevel); -void ems_setEmsRefreshed(bool b); -void ems_setWarmWaterModeComfort(uint8_t comfort); -void ems_setModels(); -void ems_setTxDisabled(bool b); +void ems_setThermostatTemp(float temperature, uint8_t temptype = 0); +void ems_setThermostatMode(uint8_t mode); +void ems_setThermostatHC(uint8_t hc); +void ems_setWarmWaterTemp(uint8_t temperature); +void ems_setFlowTemp(uint8_t temperature); +void ems_setWarmWaterActivated(bool activated); +void ems_setWarmTapWaterActivated(bool activated); +void ems_setPoll(bool b); +void ems_setLogging(_EMS_SYS_LOGGING loglevel); +void ems_setEmsRefreshed(bool b); +void ems_setWarmWaterModeComfort(uint8_t comfort); +void ems_setModels(); +void ems_setTxDisabled(bool b); +bool ems_getTxDisabled(); +uint8_t ems_getTxMode(); char * ems_getThermostatDescription(char * buffer); char * ems_getBoilerDescription(char * buffer); +char * ems_getSolarModuleDescription(char * buffer); +char * ems_getHeatPumpDescription(char * buffer); void ems_getThermostatValues(); void ems_getBoilerValues(); -void ems_getOtherValues(); +void ems_getSolarModuleValues(); bool ems_getPoll(); bool ems_getTxEnabled(); bool ems_getThermostatEnabled(); bool ems_getBoilerEnabled(); +bool ems_getSolarModuleEnabled(); +bool ems_getHeatPumpEnabled(); bool ems_getBusConnected(); _EMS_SYS_LOGGING ems_getLogging(); bool ems_getEmsRefreshed(); uint8_t ems_getThermostatModel(); +uint8_t ems_getSolarModuleModel(); void ems_discoverModels(); bool ems_getTxCapable(); uint32_t ems_getPollFrequency(); @@ -342,12 +448,12 @@ 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 _ems_clearTxData(); -int _ems_findBoilerModel(uint8_t model_id); -bool _ems_setModel(uint8_t model_id); void _removeTxQueue(); // global so can referenced in other classes -extern _EMS_Sys_Status EMS_Sys_Status; -extern _EMS_Boiler EMS_Boiler; -extern _EMS_Thermostat EMS_Thermostat; -extern _EMS_Other EMS_Other; +extern _EMS_Sys_Status EMS_Sys_Status; +extern _EMS_Boiler EMS_Boiler; +extern _EMS_Thermostat EMS_Thermostat; +extern _EMS_SolarModule EMS_SolarModule; +extern _EMS_HeatPump EMS_HeatPump; +extern _EMS_Other EMS_Other; diff --git a/src/ems_devices.h b/src/ems_devices.h index 01bc29532..f33a1c88a 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -41,14 +41,17 @@ #define EMS_OFFSET_UBASetPoints_flowtemp 0 // flow temp // Other -#define EMS_TYPE_SM10Monitor 0x97 // SM10Monitor -#define EMS_TYPE_SM100Monitor 0x0262 // SM100Monitor -#define EMS_TYPE_SM100Status 0x0264 // SM100Status -#define EMS_TYPE_SM100Status2 0x026A // SM100Status2 -#define EMS_TYPE_SM100Energy 0x028E // SM100Energy -#define EMS_TYPE_HPMonitor1 0xE3 // HeatPump Monitor 1 -#define EMS_TYPE_HPMonitor2 0xE5 // HeatPump Monitor 2 -#define EMS_TYPE_ISM1StatusMessage 0x0003 // Solar Module Junkers ISM1 Status +#define EMS_TYPE_SM10Monitor 0x97 // SM10Monitor +#define EMS_TYPE_SM100Monitor 0x0262 // SM100Monitor +#define EMS_TYPE_SM100Status 0x0264 // SM100Status +#define EMS_TYPE_SM100Status2 0x026A // SM100Status2 +#define EMS_TYPE_SM100Energy 0x028E // SM100Energy +#define EMS_TYPE_HPMonitor1 0xE3 // HeatPump Monitor 1 +#define EMS_TYPE_HPMonitor2 0xE5 // HeatPump Monitor 2 + +#define EMS_TYPE_ISM1StatusMessage 0x0003 // Solar Module Junkers ISM1 Status +#define EMS_TYPE_ISM1Set 0x0001 // for setting values of the solar module like max boiler temp +#define EMS_OFFSET_ISM1Set_MaxBoilerTemp 6 // position of max boiler temp e.g. 50 in the following example: 90 30 FF 06 00 01 50 (CRC=2C) /* * Thermostats... @@ -63,7 +66,7 @@ #define EMS_TYPE_RC10Set 0xB0 // for setting values like temp and mode #define EMS_OFFSET_RC10Set_temp 4 // position of thermostat setpoint temperature #define EMS_OFFSET_RC10StatusMessage_setpoint 1 // setpoint temp -#define EMS_OFFSET_RC10StatusMessage_curr 3 // current temp +#define EMS_OFFSET_RC10StatusMessage_curr 2 // current temp // RC20 specific #define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps @@ -109,6 +112,7 @@ #define EMS_OFFSET_RCPLUSStatusMessage_setpoint 3 // setpoint temp #define EMS_OFFSET_RCPLUSStatusMessage_curr 0 // current temp #define EMS_OFFSET_RCPLUSGet_mode_day 8 // day/night mode +#define EMS_OFFSET_RCPLUSStatusMessage_mode 0x0A // thermostat mode (auto, manual) // Junkers FR10, FW100 (EMS Plus) #define EMS_TYPE_JunkersStatusMessage 0x6F // is an automatic thermostat broadcast giving us temps @@ -118,16 +122,19 @@ // Known EMS types typedef enum { - EMS_MODEL_NONE, - EMS_MODEL_ALL, // common for all devices + EMS_MODEL_NONE, // unset + EMS_MODEL_ALL, // common for all devices - // generic ID for the boiler + // heatpump + EMS_MODEL_HP, + + // solar module + EMS_MODEL_SM, + + // boiler EMS_MODEL_UBA, - // generic ID for all the other weird devices - EMS_MODEL_OTHER, - - // and finally the thermostats + // and the thermostats EMS_MODEL_ES73, EMS_MODEL_RC10, EMS_MODEL_RC20, @@ -135,7 +142,7 @@ typedef enum { EMS_MODEL_RC30, EMS_MODEL_RC35, EMS_MODEL_EASY, - EMS_MODEL_RC310, + EMS_MODEL_RC300, EMS_MODEL_CW100, EMS_MODEL_1010, EMS_MODEL_OT, @@ -147,56 +154,71 @@ typedef enum { } _EMS_MODEL_ID; -// EMS types for known devices. This list will be extended when new devices are recognized. +// EMS types for known boilers. This list will be extended when new devices are recognized. // The device_id is always 0x08 -// format is MODEL_ID, PRODUCT ID, DESCRIPTION +// format is PRODUCT ID, DESCRIPTION const _Boiler_Type Boiler_Types[] = { - {EMS_MODEL_UBA, 72, "MC10 Module"}, - {EMS_MODEL_UBA, 123, "Buderus GB172/Nefit Trendline/Junkers Cerapur"}, - {EMS_MODEL_UBA, 115, "Nefit Topline Compact/Buderus GB162"}, - {EMS_MODEL_UBA, 203, "Buderus Logamax U122/Junkers Cerapur"}, - {EMS_MODEL_UBA, 208, "Buderus Logamax plus/GB192"}, - {EMS_MODEL_UBA, 64, "Sieger BK15/Nefit Smartline/Buderus GB152"}, - {EMS_MODEL_UBA, EMS_PRODUCTID_HEATRONICS, "Bosch Condens 2500/Junkers Heatronics3"}, // Junkers - {EMS_MODEL_UBA, 122, "Nefit Proline"}, - {EMS_MODEL_UBA, 172, "Nefit Enviline"} - -}; - -// Other EMS devices which are not considered boilers or thermostats -const _Other_Type Other_Types[] = { - - {EMS_MODEL_OTHER, 69, 0x21, "MM10 Mixer Module"}, - {EMS_MODEL_OTHER, 71, 0x11, "WM10 Switch Module"}, - {EMS_MODEL_OTHER, 160, 0x20, "MM100 Mixing Module"}, - {EMS_MODEL_OTHER, 160, 0x21, "MM100 Mixing Module"}, - {EMS_MODEL_OTHER, 159, 0x21, "MM50 Mixing Module"}, - {EMS_MODEL_OTHER, 68, 0x09, "BC10/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, 152, 0x09, "Junkers Controller"}, - {EMS_MODEL_OTHER, 205, 0x02, "Nefit Moduline Easy Connect"}, - {EMS_MODEL_OTHER, 73, EMS_ID_SM, "SM10 Solar Module"}, - {EMS_MODEL_OTHER, 163, EMS_ID_SM, "SM100 Solar Module"}, - {EMS_MODEL_OTHER, 171, 0x02, "EMS-OT OpenTherm converter"}, - {EMS_MODEL_OTHER, 252, EMS_ID_HP, "HeatPump Module"}, // warning, fake product id! - {EMS_MODEL_OTHER, 101, 0x30, "Junkers ISM1 Solar Controller"}, - {EMS_MODEL_OTHER, 189, EMS_ID_GATEWAY, "Web Gateway KM200"} + {72, "MC10 Module"}, + {123, "Buderus GB172/Nefit Trendline/Junkers Cerapur"}, + {115, "Nefit Topline Compact/Buderus GB162"}, + {203, "Buderus Logamax U122/Junkers Cerapur"}, + {208, "Buderus Logamax plus/GB192"}, + {64, "Sieger BK15/Nefit Smartline/Buderus GB152"}, + {EMS_PRODUCTID_HEATRONICS, "Bosch Condens 2500/Junkers Heatronics3"}, + {122, "Nefit Proline"}, + {172, "Nefit Enviline"} }; +/* + * Known Solar Module types + * format is PRODUCT ID, DEVICE ID, DESCRIPTION + */ +const _SolarModule_Type SolarModule_Types[] = { + + {EMS_PRODUCTID_SM10, EMS_ID_SM, "SM10 Solar Module"}, + {EMS_PRODUCTID_SM100, EMS_ID_SM, "SM100 Solar Module"}, + {EMS_PRODUCTID_ISM1, EMS_ID_SM, "Junkers ISM1 Solar Module"} + +}; + +// Other EMS devices which are not considered boilers, thermostats or solar modules +// format is PRODUCT ID, DEVICE ID, DESCRIPTION +const _Other_Type Other_Types[] = { + + {69, 0x21, "MM10 Mixer Module"}, + {71, 0x11, "WM10 Switch Module"}, + {160, 0x20, "MM100 Mixing Module"}, + {160, 0x21, "MM100 Mixing Module"}, + {159, 0x21, "MM50 Mixing Module"}, + {68, 0x09, "BC10/RFM20 Receiver"}, + {190, 0x09, "BC10 Base Controller"}, + {114, 0x09, "BC10 Base Controller"}, + {125, 0x09, "BC25 Base Controller"}, + {152, 0x09, "Junkers Controller"}, + {205, 0x02, "Nefit Moduline Easy Connect"}, + {206, 0x02, "Bosch Easy Connect"}, + {171, 0x02, "EMS-OT OpenTherm converter"}, + {252, EMS_ID_HP, "HeatPump Module"}, + {189, EMS_ID_GATEWAY, "Web Gateway KM200"} + +}; + +// heatpump +// format is PRODUCT ID, DEVICE ID, DESCRIPTION +const _HeatPump_Type HeatPump_Types[] = {{252, EMS_ID_HP, "HeatPump Module"}}; + /* * Known thermostat types and their capabilities + * format is MODEL_ID, PRODUCT ID, DEVICE ID, DESCRIPTION */ const _Thermostat_Type Thermostat_Types[] = { // Easy devices - not currently supporting write operations - {EMS_MODEL_EASY, 202, 0x18, "TC100/Nefit Easy", EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_EASY, 202, 0x18, "Logamatic TC100/Nefit Moduline Easy", EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_EASY, 203, 0x18, "Bosch EasyControl CT200", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_EASY, 206, 0x02, "Bosch Easy", EMS_THERMOSTAT_WRITE_NO}, - {EMS_MODEL_CW100, 157, 0x18, "CW100", EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_CW100, 157, 0x18, "Bosch CW100", EMS_THERMOSTAT_WRITE_NO}, // Buderus/Nefit {EMS_MODEL_RC10, 79, 0x17, "RC10/Nefit Moduline 100", EMS_THERMOSTAT_WRITE_YES}, @@ -204,7 +226,7 @@ const _Thermostat_Type Thermostat_Types[] = { {EMS_MODEL_RC20F, 93, 0x18, "RC20F", EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC30, 78, 0x10, "RC30/Nefit Moduline 400", EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC35, 86, 0x10, "RC35", EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC310, 158, 0x10, "RC3x0/Nefit Moduline 1010H", EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_RC300, 158, 0x10, "RC300/RC310/Nefit Moduline 3000", EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_1010, 165, 0x18, "Nefit Moduline 1010", EMS_THERMOSTAT_WRITE_NO}, // Sieger @@ -217,5 +239,4 @@ const _Thermostat_Type Thermostat_Types[] = { {EMS_MODEL_FR110, 108, 0x18, "Junkers FR110", EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_FW120, 192, 0x10, "Junkers FW120", EMS_THERMOSTAT_WRITE_NO} - }; diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 0afcccd56..a701d25c3 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -29,7 +29,7 @@ static void emsuart_rx_intr_handler(void * para) { EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_BUSY; // status set to busy length = 0; } - + GPIO_H(RX_MARK_MASK); // fill IRQ buffer, by emptying Rx FIFO if (USIS(EMSUART_UART) & ((1 << UIFF) | (1 << UITO) | (1 << UIBD))) { while ((USS(EMSUART_UART) >> USRXC) & 0xFF) { @@ -39,20 +39,20 @@ static void emsuart_rx_intr_handler(void * para) { // clear Rx FIFO full and Rx FIFO timeout interrupts USIC(EMSUART_UART) = (1 << UIFF) | (1 << UITO); } + GPIO_L(RX_MARK_MASK); // BREAK detection = End of EMS data block if (USIS(EMSUART_UART) & ((1 << UIBD))) { - ETS_UART_INTR_DISABLE(); // disable all interrupts and clear them - + ETS_UART_INTR_DISABLE(); // disable all interrupts and clear them USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt pEMSRxBuf->length = length; os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, length); // copy data into transfer buffer, including the BRK 0x00 at the end EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // set the status flag stating BRK has been received and we can start a new package + ETS_UART_INTR_ENABLE(); // re-enable UART interrupts system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity - - ETS_UART_INTR_ENABLE(); // re-enable UART interrupts + RX_PULSE(EMSUART_BIT_TIME / 2); } } @@ -63,21 +63,22 @@ static void emsuart_rx_intr_handler(void * para) { */ static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) { _EMSRxBuf * pCurrent = pEMSRxBuf; - uint8_t length = pCurrent->length; // number of bytes including the BRK at the end + pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; // next free EMS Receive buffer + uint8_t length = pCurrent->length; // number of bytes including the BRK at the end + pCurrent->length = 0; // validate and transmit the EMS buffer, excluding the BRK if (length == 2) { - // it's a poll or status code, single byte + RX_PULSE(20); + // it's a poll or status code, single byte and ok to send on ems_parseTelegram((uint8_t *)pCurrent->buffer, 1); - } else if ((length > 4) && (pCurrent->buffer[length - 2] != 0x00)) { + } else if ((length > 4) && (length <= EMS_MAXBUFFERSIZE + 1) && (pCurrent->buffer[length - 2] != 0x00)) { // ignore double BRK at the end, possibly from the Tx loopback // also telegrams with no data value + RX_PULSE(40); ems_parseTelegram((uint8_t *)pCurrent->buffer, length - 1); // transmit EMS buffer, excluding the BRK } - - memset(pCurrent->buffer, 0x00, EMS_MAXBUFFERSIZE); // wipe memory just to be safe - - pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; // next free EMS Receive buffer + // memset(pCurrent->buffer, 0x00, EMS_MAXBUFFERSIZE); // wipe memory just to be safe } /* @@ -94,7 +95,7 @@ static inline void ICACHE_FLASH_ATTR emsuart_flush_fifos() { */ void ICACHE_FLASH_ATTR emsuart_init() { ETS_UART_INTR_DISABLE(); - ETS_UART_INTR_ATTACH(NULL, NULL); + ETS_UART_INTR_ATTACH(nullptr, nullptr); // allocate and preset EMS Receive buffers for (int i = 0; i < EMS_MAXBUFFERS; i++) { @@ -117,11 +118,14 @@ void ICACHE_FLASH_ATTR emsuart_init() { // conf1 params // UCTOE = RX TimeOut enable (default is 1) - // UCTOT = RX TimeOut Threshold (7 bit) = want this when no more data after 2 characters (default is 2) + // UCTOT = RX TimeOut Threshold (7 bit) = want this when no more data after 1 characters (default is 2) // UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer (default was 127) // see https://www.espressif.com/sites/default/files/documentation/esp8266-technical_reference_en.pdf - USC1(EMSUART_UART) = 0; // reset config first - USC1(EMSUART_UART) = (EMS_MAX_TELEGRAM_LENGTH << UCFFT) | (0x02 << UCTOT) | (1 << UCTOE); // enable interupts + // + // change: we set UCFFT to 1 to get an immediate indicator about incoming trafffic. + // Otherwise, we're only noticed by UCTOT or RxBRK! + USC1(EMSUART_UART) = 0; // reset config first + USC1(EMSUART_UART) = (0x01 << UCFFT) | (0x01 << UCTOT) | (1 << UCTOE); // enable interupts // set interrupts for triggers USIC(EMSUART_UART) = 0xFFFF; // clear all interupts @@ -138,9 +142,9 @@ void ICACHE_FLASH_ATTR emsuart_init() { system_set_os_print(0); // swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively - system_uart_swap(); + //system_uart_swap(); - ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, NULL); + ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr); ETS_UART_INTR_ENABLE(); } @@ -160,66 +164,155 @@ void ICACHE_FLASH_ATTR emsuart_start() { } /* - * set loopback mode and clear Tx/Rx FIFO + * Send a BRK signal + * Which is a 11-bit set of zero's (11 cycles) */ -static inline void ICACHE_FLASH_ATTR emsuart_loopback(bool enable) { - if (enable) - USC0(EMSUART_UART) |= (1 << UCLBE); // enable loopback - else - USC0(EMSUART_UART) &= ~(1 << UCLBE); // disable loopback +void ICACHE_FLASH_ATTR emsuart_tx_brk() { + uint32_t tmp; + + // must make sure Tx FIFO is empty + while (((USS(EMSUART_UART) >> USTXC) & 0xFF) != 0) + ; + + tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask + USC0(EMSUART_UART) |= (tmp); // set bits + USC0(EMSUART_UART) &= ~(tmp); // clear bits + + // To create a 11-bit we set TXD_BRK bit so the break signal will + // automatically be sent when the tx fifo is empty + tmp = (1 << UCBRK); + GPIO_H(TX_MARK_MASK); + USC0(EMSUART_UART) |= (tmp); // set bit + + if (EMS_Sys_Status.emsTxMode <= 1) { // classic mode and ems+ (0, 1) + delayMicroseconds(EMSUART_TX_BRK_WAIT); + } else if (EMS_Sys_Status.emsTxMode == 3) { // junkers mode + delayMicroseconds(EMSUART_TX_WAIT_BRK - EMSUART_TX_LAG); // 1144 (11 Bits) + } + + USC0(EMSUART_UART) &= ~(tmp); // clear bit + GPIO_L(TX_MARK_MASK); } /* * Send to Tx, ending with a */ -void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { - if (len == 0) - return; +_EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { + _EMS_TX_STATUS result = EMS_TX_STATUS_OK; + if (len) { + LA_PULSE(50); + // temp code until we get mode 2 working without resets + if (EMS_Sys_Status.emsTxMode == 0) { // classic mode logic + for (uint8_t i = 0; i < len; i++) { + TX_PULSE(EMSUART_BIT_TIME / 4); + USF(EMSUART_UART) = buf[i]; + } + emsuart_tx_brk(); // send + } else if (EMS_Sys_Status.emsTxMode == 1) { // With extra tx delay for EMS+ + for (uint8_t i = 0; i < len; i++) { + TX_PULSE(EMSUART_BIT_TIME / 4); + USF(EMSUART_UART) = buf[i]; + delayMicroseconds(EMSUART_TX_BRK_WAIT); // https://github.com/proddy/EMS-ESP/issues/23# + } + emsuart_tx_brk(); // send + } else if (EMS_Sys_Status.emsTxMode == 3) { // Junkers logic by @philrich + for (uint8_t i = 0; i < len; i++) { + TX_PULSE(EMSUART_BIT_TIME / 4); + USF(EMSUART_UART) = buf[i]; - /* - * based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch - * we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO. - * after sending the last char we poll the Rx status until either - * - size(Rx FIFO) == size(Tx-Telegram) - * - is detected - * At end of receive we re-enable Rx-INT and send a Tx-BRK in loopback mode. - */ - ETS_UART_INTR_DISABLE(); // disable rx interrupt + // just to be safe wait for tx fifo empty (needed?) + while (((USS(EMSUART_UART) >> USTXC) & 0xff) != 0) + ; - // clear Rx status register - USC0(EMSUART_UART) |= (1 << UCRXRST); // reset uart rx fifo - emsuart_flush_fifos(); + // wait until bits are sent on wire + delayMicroseconds(EMSUART_TX_WAIT_BYTE - EMSUART_TX_LAG + EMSUART_TX_WAIT_GAP); + } + emsuart_tx_brk(); // send + } else if (EMS_Sys_Status.emsTxMode == 2) { + /* + * + * based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch + * we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO. + * after sending the last char we poll the Rx status until either + * - size(Rx FIFO) == size(Tx-Telegram) + * - is detected + * At end of receive we re-enable Rx-INT and send a Tx-BRK in loopback mode. + * + * EMS-Bus error handling + * 1. Busmaster stops echoing on Tx w/o permission + * 2. Busmaster cancel telegram by sending a BRK + * + * Case 1. is handled by a watchdog counter which is reset on each + * Tx attempt. The timeout should be 20x EMSUART_BIT_TIME plus + * some smart guess for processing time on targeted EMS device. + * We set EMS_Sys_Status.emsTxStatus to EMS_TX_WTD_TIMEOUT and return + * + * Case 2. is handled via a BRK chk during transmission. + * We set EMS_Sys_Status.emsTxStatus to EMS_TX_BRK_DETECT and return + * + */ - // throw out the telegram... - for (uint8_t i = 0; i < len;) { - USF(EMSUART_UART) = buf[i++]; // send each Tx byte - // wait for echo from busmaster - while ((((USS(EMSUART_UART) >> USRXC) & 0xFF) < i || (USIS(EMSUART_UART) & (1 << UIBD)))) { - delayMicroseconds(EMSUART_BIT_TIME); // burn CPU cycles... +// shorter busy poll... +#define EMSUART_BUSY_WAIT (EMSUART_BIT_TIME / 8) +#define EMS_TX_TO_COUNT ((20 + 10000 / EMSUART_BIT_TIME) * 8) + uint16_t wdc = EMS_TX_TO_COUNT; + + ETS_UART_INTR_DISABLE(); // disable rx interrupt + + // clear Rx status register + USC0(EMSUART_UART) |= (1 << UCRXRST); // reset uart rx fifo + emsuart_flush_fifos(); + + // throw out the telegram... + for (uint8_t i = 0; i < len && result == EMS_TX_STATUS_OK;) { + GPIO_H(TX_MARK_MASK); + + wdc = EMS_TX_TO_COUNT; + volatile uint8_t _usrxc = (USS(EMSUART_UART) >> USRXC) & 0xFF; + USF(EMSUART_UART) = buf[i++]; // send each Tx byte + // wait for echo from busmaster + GPIO_L(TX_MARK_MASK); + + while (((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) { + delayMicroseconds(EMSUART_BUSY_WAIT); // burn CPU cycles... + if (--wdc == 0) { + EMS_Sys_Status.emsTxStatus = result = EMS_TX_WTD_TIMEOUT; + break; + } + if (USIR(EMSUART_UART) & (1 << UIBD)) { + USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ + EMS_Sys_Status.emsTxStatus = result = EMS_TX_BRK_DETECT; + } + } + } + + // we got the whole telegram in the Rx buffer + // on Rx-BRK (bus collision), we simply enable Rx and leave it + // otherwise we send the final Tx-BRK in the loopback and re=enable Rx-INT. + // worst case, we'll see an additional Rx-BRK... + if (result != EMS_TX_STATUS_OK) { + LA_PULSE(200); // mark Tx error + } else { + // neither bus collision nor timeout - send terminating BRK signal + GPIO_H(TX_MARK_MASK); + if (!(USIS(EMSUART_UART) & (1 << UIBD))) { + // no bus collision - send terminating BRK signal + USC0(EMSUART_UART) |= (1 << UCLBE) | (1 << UCBRK); // enable loopback & set + + // wait until BRK detected... + while (!(USIR(EMSUART_UART) & (1 << UIBD))) { + delayMicroseconds(EMSUART_BUSY_WAIT); + } + + USC0(EMSUART_UART) &= ~((1 << UCBRK) | (1 << UCLBE)); // disable loopback & clear + USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ + } + GPIO_L(TX_MARK_MASK); + } + ETS_UART_INTR_ENABLE(); // receive anything from FIFO... } } - - // we got the whole telegram in the Rx buffer - // on Rx-BRK (bus collision), we simply enable Rx and leave it - // otherwise we send the final Tx-BRK in the loopback and re=enable Rx-INT. - // worst case, we'll see an additional Rx-BRK... - if (!(USIS(EMSUART_UART) & (1 << UIBD))) { - // no bus collision - send terminating BRK signal - emsuart_loopback(true); - USC0(EMSUART_UART) |= (1 << UCBRK); // set - - // wait until BRK detected... - while (!(USIS(EMSUART_UART) & (1 << UIBD))) { - delayMicroseconds(EMSUART_BIT_TIME); - } - - USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear - - USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ - emsuart_loopback(false); // disable loopback mode - } - - ETS_UART_INTR_ENABLE(); // receive anything from FIFO... + return result; } /* diff --git a/src/emsuart.h b/src/emsuart.h index cd57cd4da..0777dcfd5 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -8,16 +8,23 @@ #pragma once #include +#include #define EMSUART_UART 0 // UART 0 #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 10 // buffers for circular filling to avoid collisions -#define EMS_MAXBUFFERSIZE 32 // max size of the buffer. packets are max 32 bytes to support EMS 1.0 +#define EMS_MAXBUFFERS 5 // buffers for circular filling to avoid collisions +#define EMS_MAXBUFFERSIZE (EMS_MAX_TELEGRAM_LENGTH + 2) // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs #define EMSUART_BIT_TIME 104 // bit time @9600 baud +#define EMSUART_TX_BRK_WAIT 2070 // the BRK from Boiler master is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag) +#define EMSUART_TX_WAIT_BYTE EMSUART_BIT_TIME * 10 // Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) +#define EMSUART_TX_WAIT_BRK EMSUART_BIT_TIME * 11 // Time to send a BRK Signal (11 Bit) +#define EMSUART_TX_WAIT_GAP EMSUART_BIT_TIME * 7 // Gap between to Bytes +#define EMSUART_TX_LAG 8 + #define EMSUART_recvTaskPrio 1 #define EMSUART_recvTaskQueueLen 64 @@ -29,5 +36,5 @@ 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); +_EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len); void ICACHE_FLASH_ATTR emsuart_tx_poll(); diff --git a/src/my_config.h b/src/my_config.h index c12218e00..1dc9c16b4 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -41,12 +41,13 @@ #define THERMOSTAT_CIRCUITCALCTEMP "thermostat_circuitcalctemp" // RC35 specific // MQTT for boiler -#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT -#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running -#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 +#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT +#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running +#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 +#define TOPIC_BOILER_CMD_FLOWTEMP "boiler_cmd_flowtemp" // for received boiler flowtemp value via MQTT // MQTT for SM10/SM100 Solar Module #define TOPIC_SM_DATA "sm_data" // topic name @@ -57,6 +58,7 @@ #define SM_ENERGYLASTHOUR "energylasthour" // energy last hour #define SM_ENERGYTODAY "energytoday" // energy today #define SM_ENERGYTOTAL "energytotal" // energy total +#define SM_PUMPWORKMIN "pumpWorkMin" // Total minutes // MQTT for HP (HeatPump) #define TOPIC_HP_DATA "hp_data" // topic name @@ -83,10 +85,15 @@ // can be enabled and disabled via the 'set led' command and pin set by 'set led_gpio' #define EMSESP_LED_GPIO LED_BUILTIN +#ifdef LOGICANALYZER +#define EMSESP_DALLAS_GPIO D1 +#define EMSESP_DALLAS_PARASITE false +#else // set this if using an external temperature sensor like a DS18B20 // D5 is the default on a bbqkees board #define EMSESP_DALLAS_GPIO D5 #define EMSESP_DALLAS_PARASITE false +#endif // 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 diff --git a/src/test_data.h b/src/test_data.h index 30f9c3c31..0f9edf3b7 100644 --- a/src/test_data.h +++ b/src/test_data.h @@ -49,7 +49,10 @@ static const char * TEST_DATA[] = { "90 00 FF 00 00 6F 03 01 00 BE 00 BF", // test 44 - FR10 "08 00 E3 00 01 00 01 00 00 00 00 00 00 00 00 00 DF 00 64 55", // test 45 - heatpump Enviline "08 00 E5 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0A", // test 46 - heatpump Enviline - "38 10 FF 00 03 2B 00 C7 07 C3 01" // test 47 - heatpump Enviline + "38 10 FF 00 03 2B 00 C7 07 C3 01", // test 47 - heatpump Enviline + "08 0B 19 00 00 F7 80 00 80 00 00 00 00 00 03 58 97 0C 7B 1F 00 00 00 06 C4 DF 02 64 48 80 00", // test 48 - outdoor temp check + "88 00 19 00 00 DC 80 00 80 00 FF FF 00 00 00 21 9A 06 E1 7C 00 00 00 06 C2 13 00 1E 90 80 00", // test 49 - check max length + "30 00 FF 00 02 8E 00 00 41 82 00 00 28 36 00 00 82 21" // test 50 - SM100 }; diff --git a/src/version.h b/src/version.h index fa69ef04e..b4c75f588 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.8.0" +#define APP_VERSION "1.8.1" #define APP_HOSTNAME "ems-esp"