diff --git a/.clang-format b/.clang-format index 2228af0b9..e84ce1c3c 100644 --- a/.clang-format +++ b/.clang-format @@ -36,4 +36,6 @@ PointerAlignment: Middle SpaceAfterCStyleCast: false SpaceBeforeAssignmentOperators: true SpacesInCStyleCastParentheses: false -SpacesInParentheses: false \ No newline at end of file +SpacesInParentheses: false +DerivePointerAlignment: false +SortIncludes: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 559d0aabf..4c5ca7a42 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ node_modules # project specfic scripts/stackdmp.txt firmware + +# firmware +*.bin diff --git a/.travis.yml b/.travis.yml index fc4d9ac79..ad0bdbab8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ os: linux +dist: bionic language: python python: - - "2.7" + - "3.8" cache: directories: @@ -55,6 +56,7 @@ before_deploy: deploy: provider: releases edge: + # source: wenkokke/dpl branch: master token: ${GITHUB_TOKEN} file_glob: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 861e47c09..802d76201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,46 @@ 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.9.4] 2019-12-15 +## [1.9.5] 30-04-2020 + +### Added +- Solar Module SM200 support +- Support writing to Junkers FR100 thermostats +- Support writing to RC100, Moduline 1000/1010 thermostats +- MM10 Mixing module support (thanks @MichaelDvP) +- MM200 warm water circuits (https://github.com/proddy/EMS-ESP/pull/315) +- Support for Moduline 200 and Sieger ES72 thermostats +- First implementation of writing to generic Junker Thermostats (thanks @Neonox31) +- Added model type (Buderus, Sieger, Junkers, Nefit, Bosch, Worcester) to device names +- `set master_thermostat ` to choose with thermostat is master when there are multiple on the bus +- `boiler wwonetime` command from Telnet +- `set bus_id ` to support multiple EMS-ESP circuits. Default is 0x0B to mimic a service key. +- `mqtt_nestedjson` option to disable multiple data records being nested into a single JSON string +- MQTT publish messages are queued and gracefully published every second to avoid TCP blocks +- Added features to WW messages (0x33, 0x34) to improve WW monitoring. (PR#338 by @ypaindaveine) +- Added mixing log and stub for EMS type 0xAC (PR#338 by @ypaindaveine) +- Added Thermostat retrieving settings (0xA5) (validated on RC30N) with MQTT support (thanks Yves @ypaindaveine. See #352) +- Merged with PR https://github.com/proddy/EMS-ESP/pull/366 from @MichaelDvP fixing RC20 and MM50 + +### Fixed +- set boiler warm water temp on Junkers/Bosch HT3 +- fixed detection of the Moduline 400 thermostat +- RC35 setting temperature also forces the current select temp to change, irrespective of the mode + +### Changed +- improved MQTT publishing to stop network flooding. `publish_time` of -1 is no publish, 0 is automatic otherwise its a time interval +- External sensors (like Dallas DS18*) are sent as a nested MQTT topic including their unqiue identifier +- `mqttlog` console command renamed to `mqttqueue` to only show the current publish queue +- `status` payload on start-up shows the IP and Version of EMS-ESP +- `thermostat mode` takes a string like manual,auto,heat,day,night,eco,comfort,holiday,nofrost +- `thermostat temp` also takes a mode string, e.g. `thermostat temp 20 heat` +- `queue` renamed to `txqueue` + +### Removed + - `autodetect scan`. Replaced with `devices scan` and `devices scan+` for deep scanning + - `mqttlog all` and showing MQTT log in the web interface - no point showing history of previous mqtt publishes in ESP's precious memory. For debugging I recommend using MQTT Explorer or another external tool. + +## [1.9.4] 15-12-2019 There are breaking changes in this release. Make you sure you adjust the MQTT topics as described in the wiki. diff --git a/README.md b/README.md index f3a620931..d950f26a9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ EMS-ESP is a open-source system built for the Espressif ESP8266 microcontroller ## Features -* Supporting more than [50 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (EMS 1, EMS+/2.0 and Heatronics 3). +* Supporting more than [50 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (EMS 1, EMS 2.0/Plus and Heatronics 3). * A web interface for easy configuration and real-time monitoring of the EMS bus. * Telnet for advanced configuration and verbose traffic logging. * Configurable MQTT, with templates for Home Assistant and Domoticz. diff --git a/platformio.ini b/platformio.ini index 747006030..ffb8977db 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,16 +4,23 @@ [platformio] default_envs = release -;default_envs = debug +; default_envs = debug [common] +# From https://github.com/esp8266/Arduino/blob/master/tools/sdk/ld +# eagle.flash.4m1m.ld = 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer +# eagle.flash.4m2m.ld = same as above but with 2024 KB SPIFFS +# eagle.flash.4m.ld = same as above but with no SPIFFS storage +ldscript = eagle.flash.4m1m.ld + ; custom build options are: ; -DMYESP_TIMESTAMP ; -DTESTS ; -DCRASH ; -DFORCE_SERIAL ; -DMYESP_DEBUG -;custom_flags = -DFORCE_SERIAL -DMYESP_DEBUG +; -DSENSOR_MQTT_USEID +; custom_flags = -DFORCE_SERIAL -DMYESP_DEBUG -DEMSESP_SIMULATE custom_flags = # Available lwIP variants (macros): @@ -26,33 +33,28 @@ custom_flags = # -DVTABLES_IN_FLASH # -DNO_GLOBAL_EEPROM # -DBEARSSL_SSL_BASIC +# general_flags = -DNO_GLOBAL_EEPROM -DVTABLES_IN_FLASH -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH general_flags = -DNO_GLOBAL_EEPROM -# From https://github.com/esp8266/Arduino/blob/master/tools/sdk/ld -# eagle.flash.4m1m.ld = 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer -# eagle.flash.4m2m.ld = same as above but with 2024 KB SPIFFS -# eagle.flash.4m.ld = same as above but with no SPIFFS storage -build_flags_4m1m = -Wl,-Teagle.flash.4m1m.ld -build_flags = ${common.general_flags} ${common.build_flags_4m1m} +build_flags = ${common.general_flags} -std=c++11 -fno-exceptions [env] framework = arduino -;platform = espressif8266@2.2.2 ; arduino core 2.5.2 platform = espressif8266 -;platform = https://github.com/platformio/platform-espressif8266#develop -;platform = https://github.com/platformio/platform-espressif8266#feature/stage +board_build.ldscript = ${common.ldscript} +lib_compat_mode = strict lib_deps = https://github.com/rlogiacco/CircularBuffer https://github.com/PaulStoffregen/OneWire https://github.com/me-no-dev/ESPAsyncWebServer https://github.com/me-no-dev/ESPAsyncUDP uuid-common@^1.1.0 - uuid-log@^2.1.1 - uuid-syslog@^2.0.4 ; https://github.com/nomis/mcu-uuid-syslog + uuid-log@^2.1.1 + uuid-syslog@^2.0.4 ; https://github.com/nomis/mcu-uuid-syslog JustWifi@2.0.2 ; https://github.com/xoseperez/justwifi AsyncMqttClient@0.8.2 ; https://github.com/marvinroger/async-mqtt-client EEPROM_Rotate@0.9.2 ; https://github.com/xoseperez/eeprom_rotate - ArduinoJson@6.13.0 ; https://github.com/bblanchon/ArduinoJson + ArduinoJson ESPAsyncTCP@1.2.2 ; https://github.com/me-no-dev/ESPAsyncTCP upload_speed = 921600 monitor_speed = 115200 @@ -112,3 +114,4 @@ build_flags = ${common.build_flags} ${common.custom_flags} extra_scripts = pre:scripts/pre_script.py scripts/main_script.py + \ No newline at end of file diff --git a/scripts/main_script.py b/scripts/main_script.py index 61b873da9..cc0c8b47d 100644 --- a/scripts/main_script.py +++ b/scripts/main_script.py @@ -25,7 +25,7 @@ def clr(color, text): def remove_float_support(): flags = " ".join(env['LINKFLAGS']) - print(clr(Color.BLUE, "** LINKFLAGS = %ss" % flags)) + print(clr(Color.BLUE, "LINKFLAGS = %ss" % flags)) flags = flags.replace("-u _printf_float", "") flags = flags.replace("-u _scanf_float", "") newflags = flags.split() diff --git a/scripts/pre_script.py b/scripts/pre_script.py index ca9db79ba..4ec4bcdd3 100755 --- a/scripts/pre_script.py +++ b/scripts/pre_script.py @@ -7,7 +7,7 @@ Import("env") def build_web(): print("** Building web...") env.Execute( - "node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd ./tools/webfilesbuilder") + "node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --silent --cwd ./tools/webfilesbuilder") def code_check(source, target, env): print("** Starting cppcheck...") diff --git a/src/MyESP.cpp b/src/MyESP.cpp index eba5cde68..c67c4925f 100644 --- a/src/MyESP.cpp +++ b/src/MyESP.cpp @@ -23,6 +23,15 @@ union system_rtcmem_t { uint32_t value; }; +struct mqtt_message_t { + uint16_t packetId = 0; + char * topic = nullptr; + char * payload = nullptr; + bool retain = false; + uint8_t retry_count = 0; +}; +std::deque _mqtt_queue; + // nasty global variables that are called from internal ws functions static char * _general_password = nullptr; static bool _shouldRestart = false; @@ -74,10 +83,12 @@ MyESP::MyESP() { _mqtt_heartbeat = false; _mqtt_keepalive = MQTT_KEEPALIVE; _mqtt_qos = MQTT_QOS; + _mqtt_nestedjson = false; _mqtt_retain = MQTT_RETAIN; _mqtt_will_topic = strdup(MQTT_WILL_TOPIC); _mqtt_will_online_payload = strdup(MQTT_WILL_ONLINE_PAYLOAD); _mqtt_will_offline_payload = strdup(MQTT_WILL_OFFLINE_PAYLOAD); + _mqtt_publish_fails = 0; // count of number of failed MQTT topic publishes // network _network_password = nullptr; @@ -110,14 +121,6 @@ MyESP::MyESP() { // get the build time _buildTime = _getBuildTime(); - - // MQTT log - for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) { - MQTT_log[i].type = 0; - MQTT_log[i].timestamp = 0; - MQTT_log[i].topic = nullptr; - MQTT_log[i].payload = nullptr; - } } MyESP::~MyESP() { @@ -382,21 +385,17 @@ bool MyESP::mqttSubscribe(const char * topic) { if (mqttClient.connected() && (strlen(topic) > 0)) { char * topic_s = _mqttTopic(topic); - uint16_t packet_id = mqttClient.subscribe(topic_s, _mqtt_qos); #ifdef MYESP_DEBUG myDebug_P(PSTR("[MQTT] Subscribing to %s"), topic_s); #endif - - if (packet_id) { - // add to mqtt log - _addMQTTLog(topic_s, "", MYESP_MQTTLOGTYPE_SUBSCRIBE); // Has an empty payload for now - return true; - } else { + uint16_t packet_id = mqttClient.subscribe(topic_s, _mqtt_qos); + if (!packet_id) { myDebug_P(PSTR("[MQTT] Error subscribing to %s, error %d"), _mqttTopic(topic), packet_id); + return false; } } - return false; // didn't work + return true; } // MQTT unsubscribe @@ -407,30 +406,218 @@ void MyESP::mqttUnsubscribe(const char * topic) { } } -// Publish using the user's custom retain flag -bool MyESP::mqttPublish(const char * topic, const char * payload) { - // use the custom MQTT retain flag - return mqttPublish(topic, payload, _mqtt_retain); -} +// print MQTT log +void MyESP::_printMQTTQueue() { + myDebug_P(PSTR("MQTT publish queue:")); -// MQTT Publish -// returns true if all good -bool MyESP::mqttPublish(const char * topic, const char * payload, bool retain) { - if (mqttClient.connected() && (strlen(topic) > 0)) { -#ifdef MYESP_DEBUG - myDebug_P(PSTR("[MQTT] Sending publish to %s with payload %s"), _mqttTopic(topic), payload); -#endif - uint16_t packet_id = mqttClient.publish(_mqttTopic(topic), _mqtt_qos, retain, payload); + if (_mqtt_queue.empty()) { + myDebug_P(PSTR(" queue is empty!")); + myDebug_P(PSTR("")); // newline + return; + } - if (packet_id) { - _addMQTTLog(topic, payload, MYESP_MQTTLOGTYPE_PUBLISH); // add to the log - return true; + for (mqtt_message_t it : _mqtt_queue) { + if (it.retry_count == 0) { + if (it.packetId == 0) { + myDebug_P(PSTR(" topic=%s payload=%s"), it.topic, it.payload); + } else { + myDebug_P(PSTR(" topic=%s payload=%s (pid %d)"), it.topic, it.payload, it.packetId); + } } else { - myDebug_P(PSTR("[MQTT] Error publishing to %s with payload %s [error %d]"), _mqttTopic(topic), payload, packet_id); + myDebug_P(PSTR(" topic=%s payload=%s (pid %d, retry #%d)"), it.topic, it.payload, it.packetId, it.retry_count); } } - return false; // failed + myDebug_P(PSTR("")); // newline +} + +// Publish using the user's custom retain flag +bool MyESP::mqttPublish(const char * topic, const char * payload) { + return (_mqttQueue(topic, payload, _mqtt_retain)); +} +bool MyESP::mqttPublish(const char * topic, JsonDocument & payload) { + return (_mqttQueue(topic, payload, _mqtt_retain)); +} + +// MQTT Publish +bool MyESP::mqttPublish(const char * topic, const char * payload, bool retain) { + return (_mqttQueue(topic, payload, retain)); +} +bool MyESP::mqttPublish(const char * topic, JsonDocument & payload, bool retain) { + return (_mqttQueue(topic, payload, retain)); +} + +// put a payload string into the queue +// can't have empty topic +// returns false if can't add to queue +bool MyESP::_mqttQueue(const char * topic, const char * payload, bool retain) { + if (!mqttClient.connected() || _mqtt_queue.size() >= MQTT_QUEUE_MAX_SIZE || !_hasValue(topic)) { + return false; + } + + // create a new message + mqtt_message_t element; + element.topic = strdup(topic); + element.retain = retain; + element.packetId = 0; + element.retry_count = 0; + if (payload != NULL) { + element.payload = strdup(payload); + } +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Adding to queue: #%d [%s] %s"), _mqtt_queue.size(), element.topic, element.payload); +#endif + _mqtt_queue.push_back(element); + + return true; +} + +// convert json doc to a string buffer and place on queue +// can't have empty payload or topic +// returns false if can't add to queue +bool MyESP::_mqttQueue(const char * topic, JsonDocument & payload, bool retain) { + if (!mqttClient.connected() || _mqtt_queue.size() >= MQTT_QUEUE_MAX_SIZE || !_hasValue(topic)) { + return false; + } + + // check for empty JSON doc - we don't like those + size_t capacity = measureJson(payload); + if (!capacity) { + return false; + } + + // create a new message + mqtt_message_t element; + element.topic = strdup(topic); + element.retain = retain; + element.packetId = 0; + element.retry_count = 0; + + // reserve space for buffer and serialize json into it + capacity++; // add one more to cover the EOL + element.payload = (char *)malloc(capacity); + serializeJson(payload, (char *)element.payload, capacity); + +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Adding to queue: #%d [%s] %s"), _mqtt_queue.size(), element.topic, element.payload); +#endif + _mqtt_queue.push_back(element); + + return true; +} + +// called when an MQTT Publish ACK is received +// check if ACK matches the last Publish we sent, if not report an error +// and always remove from queue +void MyESP::_mqttOnPublish(uint16_t packetId) { +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); +#endif + + // find the MQTT message in the queue and remove it + if ((_mqtt_queue.empty()) || (_mqtt_qos == 0)) { + return; + } + + mqtt_message_t element = _mqtt_queue.front(); // get top of list + + // if the last published failed, don't bother checking it. wait for the re-try + if (element.packetId == 0) { + return; + } + + if (element.packetId == packetId) { +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Found PID %d. Removing from queue."), packetId); +#endif + } else { +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Mismatch, expecting PID %d, got %d."), element.packetId, packetId); + _mqtt_publish_fails++; // increment error count +#endif + } + + _mqttRemoveLastPublish(); // always remove +} + +// removes top of queue +void MyESP::_mqttRemoveLastPublish() { + mqtt_message_t element = _mqtt_queue.front(); // get top of list + free(element.topic); + if (element.payload) { + free(element.payload); + } + _mqtt_queue.pop_front(); +} + +// take top from queue and try and publish it +void MyESP::_mqttPublishQueue() { + if ((!mqttClient.connected()) || (_mqtt_queue.empty())) { + return; + } + + mqtt_message_t element = _mqtt_queue.front(); // fetch from queue + + // try and publish it + uint16_t packet_id = mqttClient.publish(_mqttTopic(element.topic), _mqtt_qos, element.retain, element.payload); +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Sent publish (attempt #%d, pid %d) [%s] [%s]"), element.retry_count, packet_id, _mqttTopic(element.topic), element.payload); +#endif + + if (packet_id == 0) { + // it failed. if we retried n times, give up. remove from queue + if (element.retry_count == (MQTT_PUBLISH_MAX_RETRY - 1)) { + myDebug_P(PSTR("[MQTT] Failed to publish to %s with payload %s"), _mqttTopic(element.topic), element.payload); + _mqtt_publish_fails++; // increment failure counter + _mqttRemoveLastPublish(); + } else { + _mqtt_queue[0].retry_count++; + } + return; + } + + // if we have ACK set with QOS 1 or 2, leave on queue and let the ACK process remove it + // but add the packet_id so we can check it later + if (_mqtt_qos != 0) { + _mqtt_queue[0].packetId = packet_id; +#ifdef MYESP_DEBUG + myDebug_P(PSTR("[MQTT] Setting packetID %d"), packet_id); +#endif + return; + } + + // delete it from queue + _mqttRemoveLastPublish(); +} + +// send online appended with the version information as JSON +void MyESP::_sendStartTopic() { + StaticJsonDocument doc; + JsonObject payload = doc.to(); + payload["version"] = _app_version; + payload["IP"] = WiFi.localIP().toString(); + // add time if we know it + if ((_ntp_enabled) && (NTP.tcr->abbrev != nullptr)) { + uint32_t real_time = getSystemTime(); + // exclude millis() just in case + if (real_time > 10000L) { + char s[25]; + // ISO 8601 describes an internationally accepted way to represent dates and times using numbers + snprintf_P(s, + 25, + PSTR("%04u-%02u-%02uT%02u:%02u:%02u"), + to_year(real_time), + to_month(real_time), + to_day(real_time), + to_hour(real_time), + to_minute(real_time), + to_second(real_time) + + ); + payload["boottime"] = s; + } + } + mqttPublish(MQTT_TOPIC_START, doc, true); // send with retain on } // MQTT onConnect - when a connect is established @@ -440,8 +627,8 @@ void MyESP::_mqttOnConnect() { _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; _mqtt_last_connection = millis(); - // say we're alive to the Last Will topic - mqttPublish(_mqtt_will_topic, _mqtt_will_online_payload, true); // force retain on + // say we're alive to the Last Will topic, with retain on + mqttPublish(_mqtt_will_topic, _mqtt_will_online_payload, true); // subscribe to general subs mqttSubscribe(MQTT_TOPIC_RESTART); @@ -449,10 +636,14 @@ void MyESP::_mqttOnConnect() { // subscribe to a start message and send the first publish // forcing retain to off since we only want to send this once mqttSubscribe(MQTT_TOPIC_START); - mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD, false); + + // send start topic now unless NTP is enabled, otherwise wait for the time + if (!_ntp_enabled) { + _sendStartTopic(); + } // send heartbeat if enabled - _heartbeatCheck(); + heartbeatCheck(true); // call custom function to handle mqtt receives (_mqtt_callback_f)(MQTT_CONNECT_EVENT, nullptr, nullptr); @@ -489,7 +680,8 @@ void MyESP::_mqtt_setup() { }); //mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); }); - //mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); }); + + mqttClient.onPublish([this](uint16_t packetId) { _mqttOnPublish(packetId); }); mqttClient.onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { _mqttOnMessage(topic, payload, len); @@ -502,8 +694,7 @@ void MyESP::_mqtt_setup() { // last will if (_hasValue(_mqtt_will_topic)) { - mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, - _mqtt_will_offline_payload); // retain always true + mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_offline_payload); // retain always true } // set credentials if we have them @@ -525,7 +716,7 @@ void MyESP::_wifi_setup() { WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 // ref: https://github.com/esp8266/Arduino/issues/6471 // ref: https://github.com/esp8266/Arduino/issues/6366 - // high tx power causing weird behavior, slighly lowering from 20.5 to 20.0 may help stability + // high tx power causing weird behavior, slightly lowering from 20.5 to 20.0 may help stability WiFi.setOutputPower(20.0); // in DBM #endif @@ -715,8 +906,8 @@ void MyESP::_consoleShowHelp() { myDebug_P(PSTR("*")); myDebug_P(PSTR("* Commands:")); - myDebug_P(PSTR("* ?/help=show commands, CTRL-D/quit=close telnet session")); - myDebug_P(PSTR("* set, system, restart, mqttlog, kick, save")); + myDebug_P(PSTR("* ?/help=show commands, CTRL-D/quit=end telnet session")); + myDebug_P(PSTR("* set, system, restart, mqttqueue, kick, save")); #ifdef CRASH myDebug_P(PSTR("* crash ")); @@ -749,16 +940,17 @@ bool MyESP::_hasValue(const char * s) { void MyESP::_printSetCommands() { myDebug_P(PSTR("\nset commands:\n")); myDebug_P(PSTR(" set erase")); - myDebug_P(PSTR(" set ")); + myDebug_P(PSTR(" set wifi_mode ")); myDebug_P(PSTR(" set [value]")); - myDebug_P(PSTR(" set mqtt_enabled ")); + myDebug_P(PSTR(" set mqtt_enabled [on | off]")); myDebug_P(PSTR(" set [value]")); myDebug_P(PSTR(" set mqtt_heartbeat (every 2 mins)")); - myDebug_P(PSTR(" set mqtt_base [string]")); - myDebug_P(PSTR(" set mqtt_port [number]")); + myDebug_P(PSTR(" set mqtt_base [prefix]")); + myDebug_P(PSTR(" set mqtt_port [n]")); myDebug_P(PSTR(" set mqtt_qos [0-3]")); myDebug_P(PSTR(" set mqtt_keepalive [seconds]")); myDebug_P(PSTR(" set mqtt_retain [on | off]")); + myDebug_P(PSTR(" set mqtt_nestedjson [on | off]")); myDebug_P(PSTR(" set ntp_enabled ")); myDebug_P(PSTR(" set ntp_interval [minutes]")); myDebug_P(PSTR(" set ntp_timezone [n]")); @@ -821,6 +1013,7 @@ void MyESP::_printSetCommands() { myDebug_P(PSTR(" mqtt_retain=%s"), (_mqtt_retain) ? "on" : "off"); myDebug_P(PSTR(" mqtt_qos=%d"), _mqtt_qos); myDebug_P(PSTR(" mqtt_heartbeat=%s"), (_mqtt_heartbeat) ? "on" : "off"); + myDebug_P(PSTR(" mqtt_nestedjson=%s"), (_mqtt_nestedjson) ? "on" : "off"); #ifdef FORCE_SERIAL myDebug_P(PSTR(" serial=%s (this is always when compiled with -DFORCE_SERIAL)"), (_general_serial) ? "on" : "off"); @@ -873,9 +1066,10 @@ char * MyESP::_telnet_readWord(bool allow_all_chars) { // messy code but effective since we don't have too many settings // wc is word count, number of parameters after the 'set' command bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { - bool save_config = false; - bool save_custom_config = false; - bool restart = false; + bool save_config = false; + bool restart = false; + + MYESP_FSACTION_t save_custom_config = MYESP_FSACTION_ERR; // default // check for our internal commands first if (strcmp(setting, "erase") == 0) { @@ -924,6 +1118,8 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) restart = save_config; } else if (strcmp(setting, "mqtt_heartbeat") == 0) { save_config = fs_setSettingValue(&_mqtt_heartbeat, value, false); + } else if (strcmp(setting, "mqtt_nestedjson") == 0) { + save_config = fs_setSettingValue(&_mqtt_nestedjson, value, false); } else if (strcmp(setting, "ntp_enabled") == 0) { save_config = fs_setSettingValue(&_ntp_enabled, value, false); } else if (strcmp(setting, "ntp_interval") == 0) { @@ -938,13 +1134,14 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) // finally check for any custom commands if (_fs_setlist_callback_f) { save_custom_config = (_fs_setlist_callback_f)(MYESP_FSACTION_SET, wc, setting, value); + restart = (save_custom_config == MYESP_FSACTION_RESTART); } } bool ok = false; // if we were able to recognize the set command, continue - if ((save_config || save_custom_config)) { + if ((save_config || save_custom_config != MYESP_FSACTION_ERR)) { // check for 2 params if (value == nullptr) { myDebug_P(PSTR("%s has been reset to its default value."), setting); @@ -960,7 +1157,7 @@ bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) } // and see if we need to also save for custom config - if (save_custom_config) { + if (save_custom_config != MYESP_FSACTION_ERR) { ok = _fs_createCustomConfig(); } @@ -1042,12 +1239,12 @@ void MyESP::_telnetCommand(char * commandLine) { } // print mqtt log command - if ((strcmp(ptrToCommandName, "mqttlog") == 0) && (wc == 1)) { - _printMQTTLog(); + if (strcmp(ptrToCommandName, "mqttqueue") == 0) { + _printMQTTQueue(); return; } - // show system stats + // show system status if ((strcmp(ptrToCommandName, "system") == 0) && (wc == 1)) { showSystemStats(); return; @@ -1055,12 +1252,11 @@ void MyESP::_telnetCommand(char * commandLine) { // save everything if ((strcmp(ptrToCommandName, "save") == 0) && (wc == 1)) { - _fs_writeConfig(); - _fs_createCustomConfig(); + saveSettings(); return; } - // show system stats + // quit if ((strcmp(ptrToCommandName, "quit") == 0) && (wc == 1)) { myDebug_P(PSTR("[TELNET] exiting telnet session")); SerialAndTelnet.disconnectClient(); @@ -1088,6 +1284,12 @@ void MyESP::_telnetCommand(char * commandLine) { } } +// public function so clients can save config +void MyESP::saveSettings() { + _fs_writeConfig(); + _fs_createCustomConfig(); +} + // returns WiFi hostname as a String object String MyESP::_getESPhostname() { String hostname; @@ -1323,13 +1525,13 @@ void MyESP::_systemCheckLoop() { } } -// print out ESP system stats +// print out ESP system status // for battery power is ESP.getVcc() void MyESP::showSystemStats() { #if defined(ESP8266) - myDebug_P(PSTR("%sESP8266 System stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug_P(PSTR("%sESP8266 System status:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); #else - myDebug_P(PSTR("ESP32 System stats:")); + myDebug_P(PSTR("ESP32 System status:")); #endif myDebug_P(PSTR("")); @@ -1373,13 +1575,14 @@ void MyESP::showSystemStats() { if (isMQTTConnected()) { myDebug_P(PSTR(" [MQTT] is connected (heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled"); + myDebug_P(PSTR(" [MQTT] # failed topic publishes: %d"), _mqtt_publish_fails); } else { myDebug_P(PSTR(" [MQTT] is disconnected")); } - if (_have_ntp_time) { + if ((_have_ntp_time) && (NTP.tcr->abbrev != nullptr)) { uint32_t real_time = getSystemTime(); - myDebug_P(PSTR(" [NTP] Local Time is %02d:%02d:%02d %s (%d)"), to_hour(real_time), to_minute(real_time), to_second(real_time), NTP.tcr->abbrev, real_time); + myDebug_P(PSTR(" [NTP] Local Time is %02d:%02d:%02d %s"), to_hour(real_time), to_minute(real_time), to_second(real_time), NTP.tcr->abbrev); } #ifdef CRASH @@ -1450,43 +1653,40 @@ void MyESP::showSystemStats() { /* * Send heartbeat via MQTT with all system data */ -void MyESP::_heartbeatCheck(bool force) { +void MyESP::heartbeatCheck(bool force) { static uint32_t last_heartbeat = 0; if ((millis() - last_heartbeat > MYESP_HEARTBEAT_INTERVAL) || force) { last_heartbeat = millis(); - // print to log if force is set, so at bootup - if (force) { - _printHeap("[SYSTEM]"); - } - #ifdef MYESP_DEBUG - _printHeap("[HEARTBEAT] "); + _printHeap("[HEARTBEAT]"); #endif if (!isMQTTConnected() || !(_mqtt_heartbeat)) { return; } + // print to log if force is set + if (force) { + _printHeap("[SYSTEM]"); + } + uint32_t total_memory = _getInitialFreeHeap(); uint32_t free_memory = ESP.getFreeHeap(); uint8_t mem_available = 100 * free_memory / total_memory; // as a % - StaticJsonDocument doc; - JsonObject rootHeartbeat = doc.to(); + const size_t capacity = JSON_OBJECT_SIZE(6); + StaticJsonDocument doc; + JsonObject rootHeartbeat = doc.to(); - rootHeartbeat["version"] = _app_version; - rootHeartbeat["IP"] = WiFi.localIP().toString(); - rootHeartbeat["rssid"] = getWifiQuality(); - rootHeartbeat["load"] = getSystemLoadAverage(); - rootHeartbeat["uptime"] = _getUptime(); - rootHeartbeat["freemem"] = mem_available; - rootHeartbeat["MQTTdisconnects"] = _getSystemDropoutCounter(); + rootHeartbeat["rssid"] = getWifiQuality(); + rootHeartbeat["load"] = getSystemLoadAverage(); + rootHeartbeat["uptime"] = _getUptime(); + rootHeartbeat["freemem"] = mem_available; + rootHeartbeat["tcpdrops"] = _getSystemDropoutCounter(); + rootHeartbeat["mqttpublishfails"] = _mqtt_publish_fails; - char data[300] = {0}; - serializeJson(doc, data, sizeof(data)); - - (void)mqttPublish(MQTT_TOPIC_HEARTBEAT, data, false); // send to MQTT with retain off + mqttPublish(MQTT_TOPIC_HEARTBEAT, doc, false); // send to MQTT with retain off } } @@ -1499,13 +1699,13 @@ void MyESP::heartbeatPrint() { uint32_t total_memory = _getInitialFreeHeap(); uint32_t free_memory = ESP.getFreeHeap(); - myDebug("[%d] uptime:%d bytesfree:%d (%2u%%), load:%d, dropouts:%d", - i++, - _getUptime(), - free_memory, - 100 * free_memory / total_memory, - getSystemLoadAverage(), - _getSystemDropoutCounter() + myDebug_P(PSTR("[%d] uptime:%d bytesfree:%d (%2u%%), load:%d, dropouts:%d"), + i++, + _getUptime(), + free_memory, + 100 * free_memory / total_memory, + getSystemLoadAverage(), + _getSystemDropoutCounter() ); } @@ -1738,23 +1938,25 @@ bool MyESP::_fs_loadConfig() { _general_serial = general["serial"]; #endif - JsonObject mqtt = doc["mqtt"]; - _mqtt_enabled = mqtt["enabled"]; - _mqtt_heartbeat = mqtt["heartbeat"]; - _mqtt_ip = strdup(mqtt["ip"] | ""); - _mqtt_user = strdup(mqtt["user"] | ""); - _mqtt_port = mqtt["port"] | MQTT_PORT; - _mqtt_keepalive = mqtt["keepalive"] | MQTT_KEEPALIVE; - _mqtt_retain = mqtt["retain"]; - _mqtt_qos = mqtt["qos"] | MQTT_QOS; - _mqtt_password = strdup(mqtt["password"] | ""); - _mqtt_base = strdup(mqtt["base"] | MQTT_BASE_DEFAULT); + JsonObject mqtt = doc["mqtt"]; + _mqtt_enabled = mqtt["enabled"]; + _mqtt_heartbeat = mqtt["heartbeat"]; + _mqtt_ip = strdup(mqtt["ip"] | ""); + _mqtt_user = strdup(mqtt["user"] | ""); + _mqtt_port = mqtt["port"] | MQTT_PORT; + _mqtt_keepalive = mqtt["keepalive"] | MQTT_KEEPALIVE; + _mqtt_retain = mqtt["retain"]; + _mqtt_qos = mqtt["qos"] | MQTT_QOS; + _mqtt_nestedjson = mqtt["nestedjson"] | true; // default to on + _mqtt_password = strdup(mqtt["password"] | ""); + _mqtt_base = strdup(mqtt["base"] | MQTT_BASE_DEFAULT); JsonObject ntp = doc["ntp"]; _ntp_server = strdup(ntp["server"] | ""); _ntp_interval = ntp["interval"] | 60; - if (_ntp_interval < 2) + if (_ntp_interval < 2) { _ntp_interval = NTP_INTERVAL_DEFAULT; + } _ntp_enabled = ntp["enabled"]; _ntp_timezone = ntp["timezone"] | NTP_TIMEZONE_DEFAULT; @@ -1951,17 +2153,18 @@ bool MyESP::_fs_writeConfig() { general["log_ip"] = _general_log_ip; general["version"] = _app_version; - JsonObject mqtt = doc.createNestedObject("mqtt"); - mqtt["enabled"] = _mqtt_enabled; - mqtt["heartbeat"] = _mqtt_heartbeat; - mqtt["ip"] = _mqtt_ip; - mqtt["user"] = _mqtt_user; - mqtt["port"] = _mqtt_port; - mqtt["qos"] = _mqtt_qos; - mqtt["keepalive"] = _mqtt_keepalive; - mqtt["retain"] = _mqtt_retain; - mqtt["password"] = _mqtt_password; - mqtt["base"] = _mqtt_base; + JsonObject mqtt = doc.createNestedObject("mqtt"); + mqtt["enabled"] = _mqtt_enabled; + mqtt["heartbeat"] = _mqtt_heartbeat; + mqtt["ip"] = _mqtt_ip; + mqtt["user"] = _mqtt_user; + mqtt["port"] = _mqtt_port; + mqtt["qos"] = _mqtt_qos; + mqtt["keepalive"] = _mqtt_keepalive; + mqtt["retain"] = _mqtt_retain; + mqtt["password"] = _mqtt_password; + mqtt["base"] = _mqtt_base; + mqtt["nestedjson"] = _mqtt_nestedjson; JsonObject ntp = doc.createNestedObject("ntp"); ntp["server"] = _ntp_server; @@ -2063,6 +2266,11 @@ void MyESP::_calculateLoad() { } } +// returns true if nested JSON setting is enabled +bool MyESP::mqttUseNestedJson() { + return _mqtt_nestedjson; +} + // returns true is MQTT is alive bool MyESP::isMQTTConnected() { return mqttClient.connected(); @@ -2278,7 +2486,7 @@ void MyESP::writeLogEvent(const uint8_t type, const char * msg) { // Handles WebSocket Events void MyESP::_onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t * data, size_t len) { if (type == WS_EVT_ERROR) { - myDebug("[WEB] WebSocket[%s][%u] error(%u): %s\r\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data); + myDebug_P(PSTR("[WEB] WebSocket[%s][%u] error(%u): %s\r\n"), server->url(), client->id(), *((uint16_t *)arg), (char *)data); } else if (type == WS_EVT_DATA) { AwsFrameInfo * info = (AwsFrameInfo *)arg; uint64_t index = info->index; @@ -2442,13 +2650,13 @@ void MyESP::_sendStatus() { uint32_t total_memory = _getInitialFreeHeap(); uint32_t free_memory = ESP.getFreeHeap(); - DynamicJsonDocument doc(MQTT_MAX_PAYLOAD_SIZE_LARGE); + DynamicJsonDocument doc(MYESP_JSON_MAXSIZE_MEDIUM); JsonObject root = doc.to(); root["command"] = "status"; FSInfo fsinfo; if (!SPIFFS.info(fsinfo)) { - myDebug("[SYSTEM] Error getting info on SPIFFS"); + myDebug_P(PSTR("[SYSTEM] Error getting info on SPIFFS")); } else { root["availspiffs"] = (fsinfo.totalBytes - fsinfo.usedBytes) / 1000; root["spiffssize"] = (fsinfo.totalBytes / 1000); @@ -2486,31 +2694,7 @@ void MyESP::_sendStatus() { sprintf(uptime, "%d day%s %d hour%s %d minute%s %d second%s", d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s"); root["uptime"] = uptime; - char topic_s[MQTT_MAX_TOPIC_SIZE] = {0}; - if (_hasValue(_mqtt_base)) { - strlcpy(topic_s, _mqtt_base, sizeof(topic_s)); - strlcat(topic_s, "/", sizeof(topic_s)); - strlcat(topic_s, _general_hostname, sizeof(topic_s)); - } else { - strlcpy(topic_s, _general_hostname, sizeof(topic_s)); - } - strlcat(topic_s, "/", sizeof(topic_s)); - root["mqttloghdr"] = topic_s; - - // create MQTT log - JsonArray list = root.createNestedArray("mqttlog"); - - // only send Publish - for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) { - if ((MQTT_log[i].type == 1) && (MQTT_log[i].topic != nullptr)) { - JsonObject item = list.createNestedObject(); - item["topic"] = MQTT_log[i].topic; - item["payload"] = MQTT_log[i].payload; - item["time"] = MQTT_log[i].timestamp; - } - } - - char buffer[MQTT_MAX_PAYLOAD_SIZE_LARGE]; + char buffer[MYESP_JSON_MAXSIZE_MEDIUM]; size_t len = serializeJson(root, buffer); _ws->textAll(buffer, len); @@ -2569,48 +2753,49 @@ void MyESP::_webserver_setup() { request->send(response); }); - _webServer->on("/update", - HTTP_POST, - [](AsyncWebServerRequest * request) { - AsyncWebServerResponse * response = request->beginResponse(200, "text/plain", _shouldRestart ? "OK" : "FAIL"); - response->addHeader("Connection", "close"); - request->send(response); - }, - [](AsyncWebServerRequest * request, String filename, size_t index, uint8_t * data, size_t len, bool final) { - if (!request->authenticate(MYESP_HTTP_USERNAME, _general_password)) { - return; - } - if (!index) { - ETS_UART_INTR_DISABLE(); // disable all UART interrupts to be safe - //_writeLogEvent(MYESP_SYSLOG_INFO, "Firmware update started"); - Update.runAsync(true); - if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + _webServer->on( + "/update", + HTTP_POST, + [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = request->beginResponse(200, "text/plain", _shouldRestart ? "OK" : "FAIL"); + response->addHeader("Connection", "close"); + request->send(response); + }, + [](AsyncWebServerRequest * request, String filename, size_t index, uint8_t * data, size_t len, bool final) { + if (!request->authenticate(MYESP_HTTP_USERNAME, _general_password)) { + return; + } + if (!index) { + ETS_UART_INTR_DISABLE(); // disable all UART interrupts to be safe + //_writeLogEvent(MYESP_SYSLOG_INFO, "Firmware update started"); + Update.runAsync(true); + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { //_writeLogEvent(MYESP_SYSLOG_ERROR, "Not enough space to update"); #ifdef MYESP_DEBUG - Update.printError(Serial); + Update.printError(Serial); #endif - } - } - if (!Update.hasError()) { - if (Update.write(data, len) != len) { + } + } + if (!Update.hasError()) { + if (Update.write(data, len) != len) { //_writeLogEvent(MYESP_SYSLOG_ERROR, "Writing to flash failed"); #ifdef MYESP_DEBUG - Update.printError(Serial); + Update.printError(Serial); #endif - } - } - if (final) { - if (Update.end(true)) { - //_writeLogEvent(MYESP_SYSLOG_INFO, "Firmware update finished"); - _shouldRestart = !Update.hasError(); - } else { + } + } + if (final) { + if (Update.end(true)) { + //_writeLogEvent(MYESP_SYSLOG_INFO, "Firmware update finished"); + _shouldRestart = !Update.hasError(); + } else { //_writeLogEvent(MYESP_SYSLOG_ERROR, "Firmware update failed"); #ifdef MYESP_DEBUG - Update.printError(Serial); + Update.printError(Serial); #endif - } - } - }); + } + } + }); _webServer->on("/fonts/glyphicons-halflings-regular.woff", HTTP_GET, [](AsyncWebServerRequest * request) { AsyncWebServerResponse * response = @@ -2667,84 +2852,13 @@ void MyESP::_printHeap(const char * prefix) { uint32_t total_memory = _getInitialFreeHeap(); uint32_t free_memory = ESP.getFreeHeap(); - myDebug("%s Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)", - prefix, - total_memory, - total_memory - free_memory, - 100 * (total_memory - free_memory) / total_memory, - free_memory, - 100 * free_memory / total_memory); -} - -// print MQTT log - everything that was published last per topic -void MyESP::_printMQTTLog() { - myDebug_P(PSTR("MQTT publish log:")); - uint8_t i; - - for (i = 0; i < MYESP_MQTTLOG_MAX; i++) { - if ((MQTT_log[i].topic != nullptr) && (MQTT_log[i].type == MYESP_MQTTLOGTYPE_PUBLISH)) { - myDebug_P(PSTR(" Timestamp:%02d:%02d:%02d Topic:%s Payload:%s"), - to_hour(MQTT_log[i].timestamp), - to_minute(MQTT_log[i].timestamp), - to_second(MQTT_log[i].timestamp), - MQTT_log[i].topic, - MQTT_log[i].payload); - } - } - - myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("MQTT subscriptions:")); - - for (i = 0; i < MYESP_MQTTLOG_MAX; i++) { - if ((MQTT_log[i].topic != nullptr) && (MQTT_log[i].type == MYESP_MQTTLOGTYPE_SUBSCRIBE)) { - myDebug_P(PSTR(" Topic:%s"), MQTT_log[i].topic); - } - } - - myDebug_P(PSTR("")); // newline -} - -// add an MQTT log entry to our buffer -void MyESP::_addMQTTLog(const char * topic, const char * payload, const MYESP_MQTTLOGTYPE_t type) { - static uint8_t logCount = 0; - uint8_t logPointer = 0; - bool found = false; - -#ifdef MYESP_DEBUG - myDebug("_addMQTTLog [#%d] %s (%d) [%s] (%d)", logCount, topic, strlen(topic), payload, strlen(payload)); -#endif - - // find the topic - // topics must be unique for either publish or subscribe - while ((logPointer < MYESP_MQTTLOG_MAX) && (_hasValue(MQTT_log[logPointer].topic))) { - if ((strcmp(MQTT_log[logPointer].topic, topic) == 0) && (MQTT_log[logPointer].type == type)) { - found = true; - break; - } - logPointer++; - } - - // if not found add it and increment next free space pointer - if (!found) { - logPointer = logCount; - if (++logCount == MYESP_MQTTLOG_MAX) { - logCount = 0; // rotate - } - } - - // delete old record - if (MQTT_log[logPointer].topic) { - free(MQTT_log[logPointer].topic); - } - if (MQTT_log[logPointer].payload) { - free(MQTT_log[logPointer].payload); - } - - // and add new record - MQTT_log[logPointer].type = type; - MQTT_log[logPointer].topic = strdup(topic); - MQTT_log[logPointer].payload = strdup(payload); - MQTT_log[logPointer].timestamp = now(); + myDebug_P(PSTR("%s Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)"), + prefix, + total_memory, + total_memory - free_memory, + 100 * (total_memory - free_memory) / total_memory, + free_memory, + 100 * free_memory / total_memory); } // send UTC time via ws @@ -2776,9 +2890,11 @@ void MyESP::_bootupSequence() { // check if its booted if (boot_status == MYESP_BOOTSTATUS_BOOTED) { - if ((_ntp_enabled) && (now() > 10000) && !_have_ntp_time) { + if ((_ntp_enabled) && (now() > 10000L) && !_have_ntp_time) { _have_ntp_time = true; writeLogEvent(MYESP_SYSLOG_INFO, "System booted"); + // send start topic + _sendStartTopic(); } return; } @@ -2871,7 +2987,6 @@ void MyESP::begin(const char * app_hostname, const char * app_name, const char * _webserver_setup(); // init web server _setSystemCheck(false); // reset system check - _heartbeatCheck(true); // force heartbeat, will send out message to log too _setSystemDropoutCounter(0); // reset # TCP dropouts @@ -2884,7 +2999,7 @@ void MyESP::begin(const char * app_hostname, const char * app_name, const char * void MyESP::loop() { _calculateLoad(); _systemCheckLoop(); - _heartbeatCheck(); + heartbeatCheck(); _bootupSequence(); // see if a reset was pressed during bootup jw.loop(); // WiFi @@ -2897,6 +3012,14 @@ void MyESP::loop() { _mqttConnect(); // MQTT + // every second check MQTT queue for publishing + static unsigned long lastMqttPoll = 0; + unsigned long currentMillis = millis(); + if ((unsigned long)(currentMillis - lastMqttPoll) >= MQTT_PUBLISH_WAIT) { + _mqttPublishQueue(); + lastMqttPoll = currentMillis; + } + // SysLog uuid::loop(); syslog.loop(); @@ -2907,7 +3030,7 @@ void MyESP::loop() { } if (_formatreq) { - myDebug("[SYSTEM] Factory reset initiated. Please wait. System will automatically restart when complete..."); + myDebug_P(PSTR("[SYSTEM] Factory reset initiated. Please wait. System will automatically restart when complete...")); SPIFFS.end(); _ws->enable(false); SPIFFS.format(); @@ -2917,7 +3040,7 @@ void MyESP::loop() { if (_shouldRestart) { writeLogEvent(MYESP_SYSLOG_INFO, "System is restarting"); - myDebug("[SYSTEM] Restarting..."); + myDebug_P(PSTR("[SYSTEM] Restarting...")); _deferredReset(500, CUSTOM_RESET_TERMINAL); ESP.restart(); } diff --git a/src/MyESP.h b/src/MyESP.h index b79bd64c6..5b623a4ef 100644 --- a/src/MyESP.h +++ b/src/MyESP.h @@ -9,7 +9,7 @@ #ifndef MyESP_h #define MyESP_h -#define MYESP_VERSION "1.2.22" +#define MYESP_VERSION "1.2.37" #include #include @@ -17,6 +17,7 @@ #include #include #include +#include // for MQTT publish queue // SysLog #include @@ -85,7 +86,6 @@ extern struct rst_info resetInfo; #define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most #define MQTT_TOPIC_START "start" #define MQTT_TOPIC_HEARTBEAT "heartbeat" -#define MQTT_TOPIC_START_PAYLOAD "start" #define MQTT_TOPIC_RESTART "restart" #define MQTT_WILL_ONLINE_PAYLOAD "online" // for last will & testament payload #define MQTT_WILL_OFFLINE_PAYLOAD "offline" // for last will & testament payload @@ -95,20 +95,18 @@ extern struct rst_info resetInfo; #define MQTT_QOS 0 // default qos 0 #define MQTT_WILL_TOPIC "status" // for last will & testament topic name #define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT topic -#define MQTT_MAX_PAYLOAD_SIZE 700 // max size of a JSON object. See https://arduinojson.org/v6/assistant/ -#define MQTT_MAX_PAYLOAD_SIZE_LARGE 2000 // max size of a large JSON object, like for sending MQTT log +#define MQTT_QUEUE_MAX_SIZE 50 // Size of the MQTT queue +#define MQTT_PUBLISH_WAIT 750 // time in ms before sending MQTT messages +#define MQTT_PUBLISH_MAX_RETRY 4 // max retries for giving up on publishing +#define MYESP_JSON_MAXSIZE_LARGE 2000 // for large Dynamic json files - https://arduinojson.org/v6/assistant/ +#define MYESP_JSON_MAXSIZE_MEDIUM 800 // for medium Dynamic json files - https://arduinojson.org/v6/assistant/ +#define MYESP_JSON_MAXSIZE_SMALL 200 // for smaller Static json documents - https://arduinojson.org/v6/assistant/ // Internal MQTT events #define MQTT_CONNECT_EVENT 0 #define MQTT_DISCONNECT_EVENT 1 #define MQTT_MESSAGE_EVENT 2 -#define MYESP_JSON_MAXSIZE_LARGE 2000 // for large Dynamic json files -#define MYESP_JSON_MAXSIZE_MEDIUM 800 // for medium Dynamic json files -#define MYESP_JSON_MAXSIZE_SMALL 200 // for smaller Static json documents - -#define MYESP_MQTTLOG_MAX 60 // max number of log entries for MQTT publishes and subscribes - #define MYESP_MQTT_PAYLOAD_ON '1' // for MQTT switch on #define MYESP_MQTT_PAYLOAD_OFF '0' // for MQTT switch off @@ -210,17 +208,25 @@ struct RtcmemData { static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big"); -#define MYESP_SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis (1 minute) -#define MYESP_SYSTEM_CHECK_MAX 10 // After this many crashes on boot -#define MYESP_HEARTBEAT_INTERVAL 120000 // in milliseconds, how often the MQTT heartbeat is sent (2 mins) +#define MYESP_SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis (1 min) +#define MYESP_SYSTEM_CHECK_MAX 10 // After this many crashes on boot +#define MYESP_HEARTBEAT_INTERVAL 60000 // in milliseconds, how often the MQTT heartbeat is sent (1 min) typedef struct { - bool set; // is it a set command - char key[50]; - char description[100]; + bool set; // is it a set command? + char key[60]; + char description[110]; } command_t; -typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION_t; +typedef enum { + MYESP_FSACTION_SET, + MYESP_FSACTION_LIST, + MYESP_FSACTION_SAVE, + MYESP_FSACTION_LOAD, + MYESP_FSACTION_ERR, + MYESP_FSACTION_OK, + MYESP_FSACTION_RESTART +} MYESP_FSACTION_t; typedef enum { MYESP_BOOTSTATUS_POWERON = 0, @@ -229,24 +235,14 @@ typedef enum { MYESP_BOOTSTATUS_RESETNEEDED = 3 } MYESP_BOOTSTATUS_t; // boot messages -typedef enum { MYESP_MQTTLOGTYPE_NONE, MYESP_MQTTLOGTYPE_PUBLISH, MYESP_MQTTLOGTYPE_SUBSCRIBE } MYESP_MQTTLOGTYPE_t; - -// for storing all MQTT publish messages -typedef struct { - uint8_t type; // 0=none, 1=publish, 2=subscribe - char * topic; - char * payload; - time_t timestamp; -} _MQTT_Log_t; - -typedef std::function mqtt_callback_f; -typedef std::function wifi_callback_f; -typedef std::function ota_callback_f; -typedef std::function telnetcommand_callback_f; -typedef std::function telnet_callback_f; -typedef std::function fs_loadsave_callback_f; -typedef std::function fs_setlist_callback_f; -typedef std::function web_callback_f; +typedef std::function mqtt_callback_f; +typedef std::function wifi_callback_f; +typedef std::function ota_callback_f; +typedef std::function telnetcommand_callback_f; +typedef std::function telnet_callback_f; +typedef std::function fs_loadsave_callback_f; +typedef std::function fs_setlist_callback_f; +typedef std::function web_callback_f; // calculates size of an 2d array at compile time template @@ -285,7 +281,10 @@ class MyESP { void mqttUnsubscribe(const char * topic); bool mqttPublish(const char * topic, const char * payload); bool mqttPublish(const char * topic, const char * payload, bool retain); + bool mqttPublish(const char * topic, JsonDocument & payload); + bool mqttPublish(const char * topic, JsonDocument & payload, bool retain); void setMQTT(mqtt_callback_f callback); + bool mqttUseNestedJson(); // OTA void setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post); @@ -302,6 +301,7 @@ class MyESP { // FS void setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f setlist, bool useSerial = true); + void saveSettings(); bool fs_saveConfig(JsonObject root); bool fs_saveCustomConfig(JsonObject root); bool fs_setSettingValue(char ** setting, const char * value, const char * value_default); @@ -329,25 +329,25 @@ class MyESP { uint32_t getSystemLoadAverage(); uint32_t getSystemResetReason(); uint8_t getSystemBootStatus(); - bool _have_ntp_time; unsigned long getSystemTime(); void heartbeatPrint(); + void heartbeatCheck(bool force = false); private: // mqtt - void _mqttOnMessage(char * topic, char * payload, size_t len); - void _mqttConnect(); - void _mqtt_setup(); - void _mqttOnConnect(); - void _sendStart(); - char * _mqttTopic(const char * topic); - - // mqtt log - _MQTT_Log_t MQTT_log[MYESP_MQTTLOG_MAX]; // log for publish and subscribe messages - - void _printMQTTLog(); - void _addMQTTLog(const char * topic, const char * payload, const MYESP_MQTTLOGTYPE_t type); - + void _mqttOnMessage(char * topic, char * payload, size_t len); + void _mqttOnPublish(uint16_t packetId); + void _mqttConnect(); + void _mqtt_setup(); + void _mqttOnConnect(); + void _sendStart(); + char * _mqttTopic(const char * topic); + bool _mqttQueue(const char * topic, const char * payload, bool retain); + bool _mqttQueue(const char * topic, JsonDocument & payload, bool retain); + void _printMQTTQueue(); + void _mqttPublishQueue(); + void _mqttRemoveLastPublish(); + void _sendStartTopic(); AsyncMqttClient mqttClient; // the MQTT class uint32_t _mqtt_reconnect_delay; mqtt_callback_f _mqtt_callback_f; @@ -366,6 +366,8 @@ class MyESP { uint32_t _mqtt_last_connection; bool _mqtt_connecting; bool _mqtt_heartbeat; + bool _mqtt_nestedjson; + uint16_t _mqtt_publish_fails; // wifi void _wifiCallback(justwifi_messages_t code, char * parameter); @@ -408,20 +410,18 @@ class MyESP { void _syslog_setup(); // fs and settings - void _fs_setup(); - bool _fs_loadConfig(); - bool _fs_loadCustomConfig(); - void _fs_eraseConfig(); - bool _fs_writeConfig(); - bool _fs_createCustomConfig(); - bool _fs_sendConfig(); - size_t _fs_validateConfigFile(const char * filename, size_t maxsize, JsonDocument & doc); - size_t _fs_validateLogFile(const char * filename); - + void _fs_setup(); + bool _fs_loadConfig(); + bool _fs_loadCustomConfig(); + void _fs_eraseConfig(); + bool _fs_writeConfig(); + bool _fs_createCustomConfig(); + bool _fs_sendConfig(); + size_t _fs_validateConfigFile(const char * filename, size_t maxsize, JsonDocument & doc); + size_t _fs_validateLogFile(const char * filename); fs_loadsave_callback_f _fs_loadsave_callback_f; fs_setlist_callback_f _fs_setlist_callback_f; - - void _printSetCommands(); + void _printSetCommands(); // general char * _general_hostname; @@ -444,34 +444,27 @@ class MyESP { void _kick(); // reset reason and rtcmem - bool _rtcmem_status; - bool _rtcmemStatus(); - bool _getRtcmemStatus(); - - void _rtcmemInit(); - void _rtcmemSetup(); - - void _deferredReset(unsigned long delay, uint8_t reason); - + bool _rtcmem_status; + bool _rtcmemStatus(); + bool _getRtcmemStatus(); + void _rtcmemInit(); + void _rtcmemSetup(); + void _deferredReset(unsigned long delay, uint8_t reason); uint8_t _getSystemStabilityCounter(); void _setSystemStabilityCounter(uint8_t counter); - uint8_t _getSystemDropoutCounter(); void _setSystemDropoutCounter(uint8_t counter); void _increaseSystemDropoutCounter(); - void _setSystemResetReason(uint8_t reason); uint8_t _getCustomResetReason(); void _setCustomResetReason(uint8_t reason); uint8_t _getSystemResetReason(); - - void _setSystemBootStatus(uint8_t status); - - bool _systemStable; - void _bootupSequence(); - bool _getSystemCheck(); - void _systemCheckLoop(); - void _setSystemCheck(bool stable); + void _setSystemBootStatus(uint8_t status); + bool _systemStable; + void _bootupSequence(); + bool _getSystemCheck(); + void _systemCheckLoop(); + void _setSystemCheck(bool stable); // load average (0..100) and heap ram void _calculateLoad(); @@ -479,9 +472,6 @@ class MyESP { uint32_t _getInitialFreeHeap(); uint32_t _getUsedHeap(); - // heartbeat - void _heartbeatCheck(bool force = false); - // web web_callback_f _web_callback_f; const char * _http_username; @@ -500,6 +490,7 @@ class MyESP { uint16_t _ntp_interval; bool _ntp_enabled; uint8_t _ntp_timezone; + bool _have_ntp_time; }; extern MyESP myESP; diff --git a/src/Ntp.cpp b/src/Ntp.cpp index bb3154670..e01d6b7db 100644 --- a/src/Ntp.cpp +++ b/src/Ntp.cpp @@ -110,16 +110,16 @@ time_t ICACHE_FLASH_ATTR NtpClient::getNtpTime() { time_t UnixUTCtime = (highWord << 16 | lowWord) - 2208988800UL; time_t adjustedtime = (*tz).toLocal(UnixUTCtime, &tcr); - myESP.myDebug("[NTP] Internet time: %02d:%02d:%02d UTC on %d/%d. Local time: %02d:%02d:%02d %s", - to_hour(UnixUTCtime), - to_minute(UnixUTCtime), - to_second(UnixUTCtime), - to_day(UnixUTCtime), - to_month(UnixUTCtime), - to_hour(adjustedtime), - to_minute(adjustedtime), - to_second(adjustedtime), - tcr->abbrev); + myESP.myDebug_P(PSTR("[NTP] Internet time: %02d:%02d:%02d UTC on %d/%d. Local time: %02d:%02d:%02d %s"), + to_hour(UnixUTCtime), + to_minute(UnixUTCtime), + to_second(UnixUTCtime), + to_day(UnixUTCtime), + to_month(UnixUTCtime), + to_hour(adjustedtime), + to_minute(adjustedtime), + to_second(adjustedtime), + tcr->abbrev); setTime(adjustedtime); }); diff --git a/src/custom.htm b/src/custom.htm index ad1e4d745..ebf09f08d 100644 --- a/src/custom.htm +++ b/src/custom.htm @@ -114,7 +114,7 @@
+ data-content="How often to send the MQTT topics in seconds. Must be at least 1"> @@ -192,10 +192,8 @@ - Boiler Temperature: + DHW Temperature: - Return Temperature: -
diff --git a/src/custom.js b/src/custom.js index 5d8a16fd7..41535fffb 100644 --- a/src/custom.js +++ b/src/custom.js @@ -8,7 +8,7 @@ var custom_config = { "listen_mode": false, "shower_timer": false, "shower_alert": false, - "publish_time": 0, + "publish_time": 10, "tx_mode": 1 } }; @@ -99,7 +99,7 @@ function listCustomStats() { var l = document.createElement("li"); var type = obj[i].type; var color = ""; - if (type === "UBAMaster") { + if (type === "Boiler") { color = "list-group-item-success"; } else if (type === "Thermostat") { color = "list-group-item-info"; @@ -108,7 +108,7 @@ function listCustomStats() { } else if (type === "Heat Pump") { color = "list-group-item-success"; } - l.innerHTML = obj[i].model + " (Version:" + obj[i].version + " ProductID:" + obj[i].productid + " DeviceID:0x" + obj[i].deviceid + ")"; + l.innerHTML = obj[i].model + " (DeviceID: 0x" + obj[i].deviceid + ", ProductID: " + obj[i].productid + ", Version: " + obj[i].version + ")"; l.className = "list-group-item " + color; list.appendChild(l); } @@ -122,7 +122,6 @@ function listCustomStats() { document.getElementById("b3").innerHTML = ajaxobj.boiler.b3 + " ℃"; document.getElementById("b4").innerHTML = ajaxobj.boiler.b4 + " ℃"; document.getElementById("b5").innerHTML = ajaxobj.boiler.b5 + " ℃"; - document.getElementById("b6").innerHTML = ajaxobj.boiler.b6 + " ℃"; } else { document.getElementById("boiler_show").style.display = "none"; } diff --git a/src/ds18.cpp b/src/ds18.cpp index fbaa1ed55..ddb22cc40 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -18,24 +18,28 @@ DS18::DS18() { } DS18::~DS18() { - if (_wire) + if (_wire) { delete _wire; + } } // init -uint8_t DS18::setup(uint8_t gpio, bool parasite) { - uint8_t count; - +void DS18::setup(uint8_t gpio, bool parasite) { _gpio = gpio; _parasite = (parasite ? 1 : 0); // OneWire - if (_wire) + if (_wire) { delete _wire; + } _wire = new OneWire(_gpio); +} - // Search devices - count = loadDevices(); +// clear list and scan for devices +uint8_t DS18::scan() { + _devices.clear(); + + uint8_t count = loadDevices(); // start the search // If no devices found check again pulling up the line if (count == 0) { @@ -48,30 +52,22 @@ uint8_t DS18::setup(uint8_t gpio, bool parasite) { return count; } -// scan every 2 seconds void DS18::loop() { - static uint32_t last = 0; - if (millis() - last < DS18_READ_INTERVAL) - return; - last = millis(); - - // Every second we either start a conversion or read the scratchpad + // we either start a conversion or read the scratchpad static bool conversion = true; if (conversion) { - // Start conversion _wire->reset(); _wire->skip(); _wire->write(DS18_CMD_START_CONVERSION, _parasite); } else { // Read scratchpads for (unsigned char index = 0; index < _devices.size(); index++) { - // Read scratchpad if (_wire->reset() == 0) { - // Force a CRC check error - _devices[index].data[0] = _devices[index].data[0] + 1; + _devices[index].data[0] = _devices[index].data[0] + 1; // Force a CRC check error return; } + // Read each scratchpad _wire->select(_devices[index].address); _wire->write(DS18_CMD_READ_SCRATCHPAD); @@ -81,8 +77,7 @@ void DS18::loop() { } if (_wire->reset() != 1) { - // Force a CRC check error - _devices[index].data[0] = _devices[index].data[0] + 1; + _devices[index].data[0] = _devices[index].data[0] + 1; // Force a CRC check error return; } @@ -94,7 +89,7 @@ void DS18::loop() { } // return string of the device, with name and address -char * DS18::getDeviceString(char * buffer, unsigned char index) { +char * DS18::getDeviceType(char * buffer, unsigned char index) { uint8_t size = 128; if (index < _count) { unsigned char chip_id = chip(index); @@ -109,25 +104,22 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { } else { strlcpy(buffer, "Unknown", size); } + } else { + strlcpy(buffer, "invalid", size); + } - /* + return buffer; +} + +// return string of the device, with name and address +char * DS18::getDeviceID(char * buffer, unsigned char index) { + uint8_t size = 128; + if (index < _count) { uint8_t * address = _devices[index].address; - char a[30] = {0}; - snprintf(a, - sizeof(a), - " (%02X%02X%02X%02X%02X%02X%02X%02X) @ GPIO%d", - address[0], - address[1], - address[2], - address[3], - address[4], - address[5], - address[6], - address[7], - _gpio); + char a[30] = {0}; + snprintf(a, sizeof(a), "%02X%02X%02X%02X%02X%02X%02X%02X", address[0], address[1], address[2], address[3], address[4], address[5], address[6], address[7]); - strlcat(buffer, a, size); - */ + strlcpy(buffer, a, size); } else { strlcpy(buffer, "invalid", size); } @@ -153,8 +145,9 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { byte 8: SCRATCHPAD_CRC */ int16_t DS18::getRawValue(unsigned char index) { - if (index >= _count) + if (index >= _count) { return 0; + } uint8_t * data = _devices[index].data; @@ -170,23 +163,35 @@ int16_t DS18::getRawValue(unsigned char index) { } } else { byte cfg = (data[4] & 0x60); - if (cfg == 0x00) + if (cfg == 0x00) { raw = raw & ~7; // 9 bit res, 93.75 ms - else if (cfg == 0x20) + } else if (cfg == 0x20) { raw = raw & ~3; // 10 bit res, 187.5 ms - else if (cfg == 0x40) + } else if (cfg == 0x40) { raw = raw & ~1; // 11 bit res, 375 ms // 12 bit res, 750 ms + } } return raw; } -// return real value as a float -// The raw temperature data is in units of sixteenths of a degree, so the value must be divided by 16 in order to convert it to degrees. +// return real value as a float, rounded to 2 decimal places float DS18::getValue(unsigned char index) { - float value = (float)getRawValue(index) / 16.0; - return value; + int16_t raw_value = getRawValue(index); + + // check if valid + if ((raw_value == DS18_CRC_ERROR) || (raw_value == DS18_DISCONNECTED)) { + return (float)DS18_DISCONNECTED; + } + + // The raw temperature data is in units of sixteenths of a degree, + // so the value must first be divided by 16 in order to convert it to degrees. + float new_value = (float)(raw_value / 16.0); + + // round to 2 decimal places + // https://arduinojson.org/v6/faq/how-to-configure-the-serialization-of-floats/ + return (int)(new_value * 100 + 0.5) / 100.0; } // check for a supported DS chip version @@ -196,8 +201,9 @@ bool DS18::validateID(unsigned char id) { // return the type unsigned char DS18::chip(unsigned char index) { - if (index < _count) + if (index < _count) { return _devices[index].address[0]; + } return 0; } @@ -206,6 +212,7 @@ uint8_t DS18::loadDevices() { uint8_t address[8]; _wire->reset(); _wire->reset_search(); + while (_wire->search(address)) { // Check CRC if (_wire->crc8(address, 7) == address[7]) { diff --git a/src/ds18.h b/src/ds18.h index 3e2dd717e..8fbfc1141 100644 --- a/src/ds18.h +++ b/src/ds18.h @@ -36,9 +36,11 @@ class DS18 { DS18(); ~DS18(); - uint8_t setup(uint8_t gpio, bool parasite); + void setup(uint8_t gpio, bool parasite); + uint8_t scan(); void loop(); - char * getDeviceString(char * s, unsigned char index); + char * getDeviceType(char * s, unsigned char index); + char * getDeviceID(char * buffer, unsigned char index); float getValue(unsigned char index); int16_t getRawValue(unsigned char index); // raw values, needs / 16 diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index 6ef36abf5..bd5d7d37b 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -19,6 +19,7 @@ // Dallas external temp sensors #include "ds18.h" DS18 ds18; +#define DS18_MQTT_PAYLOAD_MAXSIZE 600 // public libraries #include // https://github.com/bblanchon/ArduinoJson @@ -32,10 +33,9 @@ DS18 ds18; #define APP_URL "https://github.com/proddy/EMS-ESP" #define APP_URL_API "https://api.github.com/repos/proddy/EMS-ESP" -// timers, all values are in seconds -#define DEFAULT_PUBLISHTIME 0 +#define DEFAULT_PUBLISHTIME 0 // 0=automatic +#define DEFAULT_SENSOR_PUBLISHTIME 10 // 10 seconds to post MQTT topics on Dallas sensors when in automatic mode Ticker publishValuesTimer; -Ticker publishSensorValuesTimer; bool _need_first_publish = true; // this ensures on boot we always send out MQTT messages @@ -45,6 +45,9 @@ Ticker systemCheckTimer; #define REGULARUPDATES_TIME 60 // every minute a call is made to fetch data from EMS devices manually Ticker regularUpdatesTimer; +#define DAILYUPDATES_TIME 86400 // every day a call is made to fetch data from EMS devices manually +Ticker dailyUpdatesTimer; + #define LEDCHECK_TIME 500 // every 1/2 second blink the heartbeat LED Ticker ledcheckTimer; @@ -70,15 +73,18 @@ typedef struct { uint8_t dallas_sensors; // count of dallas sensors // custom params - bool shower_timer; // true if we want to report back on shower times - bool shower_alert; // true if we want the alert of cold water - bool led; // LED on/off - bool listen_mode; // stop automatic Tx on/off - 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 - uint8_t tx_mode; // TX mode 1,2 or 3 + bool shower_timer; // true if we want to report back on shower times + bool shower_alert; // true if we want the alert of cold water + bool led; // LED on/off + bool listen_mode; // stop automatic Tx on/off + int16_t publish_time; // frequency of MQTT publish in seconds, -1 for off + 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 + uint8_t tx_mode; // TX mode 1,2 or 3 + uint8_t bus_id; // BUS ID, defaults to 0x0B for the service key + uint8_t master_thermostat; // Product ID of master thermostat to use, 0 for automatic + char * known_devices; // list of known deviceIDs for quick boot } _EMSESP_Settings; typedef struct { @@ -98,27 +104,24 @@ static const command_t project_cmds[] PROGMEM = { {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=automatic)"}, + {true, "publish_time ", "set frequency for publishing data to MQTT (-1=off, 0=automatic)"}, {true, "tx_mode ", "changes Tx logic. 1=EMS generic, 2=EMS+, 3=HT3"}, - + {true, "bus_id ", "EMS-ESP's deviceID. 0B=Service Key (default), 0D=Modem, 0A=Hand terminal, 0F=Time module, 12=Error module"}, + {true, "master_thermostat [product id]", "set default thermostat to use. No argument lists options"}, {false, "info", "show current values deciphered from the EMS messages"}, - {false, "log ", "insert a test telegram on to the EMS bus"}, -#endif - + {false, "log ", "logging: none, basic, thermo, solar, mixing, raw, jabber, verbose, watch a type or device"}, {false, "publish", "publish all values to MQTT"}, {false, "refresh", "fetch values from the EMS devices"}, - {false, "devices", "list detected EMS devices"}, - {false, "queue", "show current Tx queue"}, - {false, "autodetect [scan]", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, + {false, "devices [scan] | [scan+] | [clear] | [save] ", "list detected devices, quick scan, deep scan, clear and and save"}, + {false, "txqueue", "show current Tx queue"}, {false, "send XX ...", "send raw telegram data to EMS bus (XX are hex values)"}, {false, "thermostat read ", "send read request to the thermostat for heating circuit hc 1-4"}, - {false, "thermostat temp [hc] ", "set current thermostat temperature"}, - {false, "thermostat mode [hc] ", "set mode (0=off, 1=manual, 2=auto) for heating circuit hc 1-4"}, + {false, "thermostat temp [mode] [hc]", "set thermostat temperature. mode is manual,auto,heat,day,night,eco,comfort,holiday,nofrost"}, + {false, "thermostat mode [hc]", "set mode (manual,auto,heat,day,night,eco,comfort,holiday,nofrost)"}, {false, "boiler read ", "send read request to boiler"}, {false, "boiler wwtemp ", "set boiler warm water temperature"}, + {false, "boiler wwactive ", "set boiler warm water on/off"}, + {false, "boiler wwonetime ", "set boiler warm water onetime on/off"}, {false, "boiler tapwater ", "set boiler warm tap water on/off"}, {false, "boiler flowtemp ", "set boiler flow temperature"}, {false, "boiler comfort ", "set boiler warm water comfort setting"} @@ -144,7 +147,7 @@ _EMS_THERMOSTAT_MODE _getThermostatMode(uint8_t hc_num) { _EMS_THERMOSTAT_MODE thermoMode = EMS_THERMOSTAT_MODE_UNKNOWN; uint8_t mode = EMS_Thermostat.hc[hc_num - 1].mode; - uint8_t model = ems_getThermostatModel(); + uint8_t model = ems_getThermostatFlags(); if (model == EMS_DEVICE_FLAG_RC20) { if (mode == 0) { @@ -154,13 +157,13 @@ _EMS_THERMOSTAT_MODE _getThermostatMode(uint8_t hc_num) { } else if (mode == 2) { thermoMode = EMS_THERMOSTAT_MODE_AUTO; } - } else if (model == EMS_DEVICE_FLAG_RC300) { + } else if ((model == EMS_DEVICE_FLAG_RC300) || (model == EMS_DEVICE_FLAG_RC100)) { if (mode == 0) { thermoMode = EMS_THERMOSTAT_MODE_MANUAL; } else if (mode == 1) { thermoMode = EMS_THERMOSTAT_MODE_AUTO; } - } else if (model == EMS_DEVICE_FLAG_JUNKERS) { + } else if ((model == EMS_DEVICE_FLAG_JUNKERS1) || (model == EMS_DEVICE_FLAG_JUNKERS2)) { if (mode == 1) { thermoMode = EMS_THERMOSTAT_MODE_MANUAL; } else if (mode == 2) { @@ -180,21 +183,21 @@ _EMS_THERMOSTAT_MODE _getThermostatMode(uint8_t hc_num) { } // figures out the thermostat day/night mode depending on the thermostat type -// returns {EMS_THERMOSTAT_MODE_NIGHT, EMS_THERMOSTAT_MODE_DAY} +// returns {EMS_THERMOSTAT_MODE_NIGHT, EMS_THERMOSTAT_MODE_DAY, etc...} // hc_num is 1 to 4 -_EMS_THERMOSTAT_MODE _getThermostatDayMode(uint8_t hc_num) { +_EMS_THERMOSTAT_MODE _getThermostatMode2(uint8_t hc_num) { _EMS_THERMOSTAT_MODE thermoMode = EMS_THERMOSTAT_MODE_UNKNOWN; - uint8_t model = ems_getThermostatModel(); + uint8_t model = ems_getThermostatFlags(); - uint8_t mode = EMS_Thermostat.hc[hc_num - 1].day_mode; + uint8_t mode = EMS_Thermostat.hc[hc_num - 1].mode_type; - if (model == EMS_DEVICE_FLAG_JUNKERS) { + if ((model == EMS_DEVICE_FLAG_JUNKERS1) || (model == EMS_DEVICE_FLAG_JUNKERS2)) { if (mode == 3) { thermoMode = EMS_THERMOSTAT_MODE_DAY; } else if (mode == 2) { thermoMode = EMS_THERMOSTAT_MODE_NIGHT; } - } else if (model == EMS_DEVICE_FLAG_RC35) { + } else if ((model == EMS_DEVICE_FLAG_RC35) || (model == EMS_DEVICE_FLAG_RC30N)) { if (mode == 0) { thermoMode = EMS_THERMOSTAT_MODE_NIGHT; } else if (mode == 1) { @@ -202,22 +205,22 @@ _EMS_THERMOSTAT_MODE _getThermostatDayMode(uint8_t hc_num) { } } else if (model == EMS_DEVICE_FLAG_RC300) { if (mode == 0) { - thermoMode = EMS_THERMOSTAT_MODE_NIGHT; + thermoMode = EMS_THERMOSTAT_MODE_ECO; } else if (mode == 1) { - thermoMode = EMS_THERMOSTAT_MODE_DAY; + thermoMode = EMS_THERMOSTAT_MODE_COMFORT; } + } else if (model == EMS_DEVICE_FLAG_RC100) { + thermoMode = EMS_THERMOSTAT_MODE_DAY; // no modes on these devices } return thermoMode; } -// Info - display stats on an 'info' command +// Info - display status and data on an 'info' command void showInfo() { - // General stats from EMS bus + static char buffer_type[200] = {0}; - static char buffer_type[128] = {0}; - - myDebug_P(PSTR("%sEMS-ESP system stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug_P(PSTR("%sEMS-ESP system status:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); _EMS_SYS_LOGGING sysLog = ems_getLogging(); if (sysLog == EMS_SYS_LOGGING_BASIC) { myDebug_P(PSTR(" System logging set to Basic")); @@ -227,10 +230,14 @@ void showInfo() { 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 if (sysLog == EMS_SYS_LOGGING_MIXINGMODULE) { + myDebug_P(PSTR(" System logging set to Mixing Module only")); } else if (sysLog == EMS_SYS_LOGGING_JABBER) { myDebug_P(PSTR(" System logging set to Jabber")); } else if (sysLog == EMS_SYS_LOGGING_WATCH) { myDebug_P(PSTR(" System logging set to Watch")); + } else if (sysLog == EMS_SYS_LOGGING_DEVICE) { + myDebug_P(PSTR(" System logging set to device")); } else { myDebug_P(PSTR(" System logging set to None")); } @@ -244,16 +251,22 @@ void showInfo() { (ems_getBoilerEnabled() ? "enabled" : "disabled"), (ems_getThermostatEnabled() ? "enabled" : "disabled"), (ems_getSolarModuleEnabled() ? "enabled" : "disabled"), - (ems_getMixingDeviceEnabled() ? "enabled" : "disabled")); + (ems_getMixingModuleEnabled() ? "enabled" : "disabled")); myDebug_P(PSTR(" Shower Timer: %s, Shower Alert: %s"), ((EMSESP_Settings.shower_timer) ? "enabled" : "disabled"), ((EMSESP_Settings.shower_alert) ? "enabled" : "disabled")); - myDebug_P(PSTR("\n%sEMS Bus stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); + if (strlen(EMSESP_Settings.known_devices) > 0) { + myDebug_P(PSTR(" Saved known device IDs: %s"), EMSESP_Settings.known_devices); + } else { + myDebug_P(PSTR(" Saved known device IDs: none")); + } + + myDebug_P(PSTR("\n%sEMS Bus status:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); if (ems_getBusConnected()) { - myDebug_P(PSTR(" Bus is connected, protocol: %s"), ((EMS_Sys_Status.emsIDMask == 0x80) ? "HT3" : "Buderus")); + myDebug_P(PSTR(" Bus is connected, protocol: %s"), (ems_isHT3() ? "HT3" : "Buderus")); myDebug_P(PSTR(" Rx: # successful read requests=%d, # CRC errors=%d"), EMS_Sys_Status.emsRxPgks, EMS_Sys_Status.emxCrcErr); if (ems_getTxCapable()) { @@ -269,146 +282,224 @@ void showInfo() { } myDebug_P(PSTR("")); - myDebug_P(PSTR("%sBoiler stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - // version details - myDebug_P(PSTR(" Boiler: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_BOILER, buffer_type)); + // show boiler data if connected + if (ems_getBoilerEnabled()) { + myDebug_P(PSTR("%sBoiler data:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - // active stats - if (ems_getBusConnected()) { - if (EMS_Boiler.tapwaterActive != EMS_VALUE_INT_NOTSET) { - myDebug_P(PSTR(" Hot tap water: %s"), EMS_Boiler.tapwaterActive ? "running" : "off"); + // version details + myDebug_P(PSTR(" Boiler: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_BOILER, buffer_type)); + + if (ems_getBusConnected()) { + if (EMS_Boiler.tapwaterActive != EMS_VALUE_INT_NOTSET) { + myDebug_P(PSTR(" Hot tap water: %s"), EMS_Boiler.tapwaterActive ? "running" : "off"); + } + + if (EMS_Boiler.heatingActive != EMS_VALUE_INT_NOTSET) { + myDebug_P(PSTR(" Central heating: %s"), EMS_Boiler.heatingActive ? "active" : "off"); + } } - if (EMS_Boiler.heatingActive != EMS_VALUE_INT_NOTSET) { - myDebug_P(PSTR(" Central heating: %s"), EMS_Boiler.heatingActive ? "active" : "off"); + // UBAParameterWW + _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); + _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); + myDebug_P(PSTR(" Warm Water circulation pump type: %s"), EMS_Boiler.wWCircPumpType ? "3-way pump" : "charge pump"); + if (EMS_Boiler.wWCircPumpMode == 7) { + myDebug_P(PSTR(" Warm Water circulation pump freq: continuous")); + } else { + char s[7]; + char buffer[2]; + buffer[0] = (EMS_Boiler.wWCircPumpMode % 10) + '0'; + buffer[1] = '\0'; + strlcpy(s, buffer, 3); + strlcat(s, "x3min", sizeof(s)); + myDebug_P(PSTR(" Warm Water circulation pump freq: %s"), s); + } + if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { + myDebug_P(PSTR(" Warm Water comfort setting: Hot")); + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { + myDebug_P(PSTR(" Warm Water comfort setting: Eco")); + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { + myDebug_P(PSTR(" Warm Water comfort setting: Intelligent")); + } + + _renderIntValue("Warm Water selected temperature", "C", EMS_Boiler.wWSelTemp); + _renderIntValue("Warm Water desinfection temperature", "C", EMS_Boiler.wWDesinfectTemp); + _renderBoolValue("Warm Water circulation active", EMS_Boiler.wWCirc); + + // UBAMonitorWWMessage + _renderIntValue("Warm Water set temperature", "C", EMS_Boiler.wWSetTmp); + _renderUShortValue("Warm Water current temperature", "C", EMS_Boiler.wWCurTmp); + _renderUShortValue("Warm water temperature (intern)", "C", EMS_Boiler.wwStorageTemp1); + _renderUShortValue("Warm water temperature (extern)", "C", EMS_Boiler.wwStorageTemp2); + _renderUShortValue("Warm Water current temperature (extern)", "C", EMS_Boiler.wWCurTmp2); + _renderIntValue("Warm Water current tap water flow", "l/min", EMS_Boiler.wWCurFlow, 10); + _renderLongValue("Warm Water # starts", "times", EMS_Boiler.wWStarts); + if (EMS_Boiler.wWWorkM != EMS_VALUE_LONG_NOTSET) { + myDebug_P(PSTR(" Warm Water active time: %d days %d hours %d minutes"), + EMS_Boiler.wWWorkM / 1440, + (EMS_Boiler.wWWorkM % 1440) / 60, + EMS_Boiler.wWWorkM % 60); + } + _renderBoolValue("Warm Water 3-way valve", EMS_Boiler.wWHeat); + + // UBAMonitorFast + _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); + _renderUShortValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); + _renderUShortValue("Max boiler temperature", "C", EMS_Boiler.boilTemp); + _renderUShortValue("Return temperature", "C", EMS_Boiler.retTemp); + _renderBoolValue("Gas", EMS_Boiler.burnGas); + _renderBoolValue("Warm water mode", EMS_Boiler.wWMode); + _renderBoolValue("Boiler pump", EMS_Boiler.heatPmp); + _renderBoolValue("Fan", EMS_Boiler.fanWork); + _renderBoolValue("Ignition", EMS_Boiler.ignWork); + _renderBoolValue("Circulation pump", EMS_Boiler.wWCirc); + _renderIntValue("Burner selected max power", "%", EMS_Boiler.selBurnPow); + _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); + _renderShortValue("Flame current", "uA", EMS_Boiler.flameCurr); + _renderIntValue("System pressure", "bar", EMS_Boiler.sysPress, 10); + if (EMS_Boiler.serviceCode == EMS_VALUE_USHORT_NOTSET) { + myDebug_P(PSTR(" System service code: %s"), EMS_Boiler.serviceCodeChar); + } else { + myDebug_P(PSTR(" System service code: %s (%d)"), EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); + } + + // UBAParametersMessage + _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); + _renderIntValue("Boiler circuit pump modulation max power", "%", EMS_Boiler.pump_mod_max); + _renderIntValue("Boiler circuit pump modulation min power", "%", EMS_Boiler.pump_mod_min); + + // UBAMonitorSlow + if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) { + _renderShortValue("Outside temperature", "C", EMS_Boiler.extTemp); + } + + _renderUShortValue("Exhaust temperature", "C", EMS_Boiler.exhaustTemp); + _renderIntValue("Pump modulation", "%", EMS_Boiler.pumpMod); + _renderLongValue("Burner # starts", "times", EMS_Boiler.burnStarts); + if (EMS_Boiler.burnWorkMin != EMS_VALUE_LONG_NOTSET) { + myDebug_P(PSTR(" Total burner operating time: %d days %d hours %d minutes"), + EMS_Boiler.burnWorkMin / 1440, + (EMS_Boiler.burnWorkMin % 1440) / 60, + EMS_Boiler.burnWorkMin % 60); + } + if (EMS_Boiler.heatWorkMin != EMS_VALUE_LONG_NOTSET) { + myDebug_P(PSTR(" Total heat operating time: %d days %d hours %d minutes"), + EMS_Boiler.heatWorkMin / 1440, + (EMS_Boiler.heatWorkMin % 1440) / 60, + EMS_Boiler.heatWorkMin % 60); + } + if (EMS_Boiler.UBAuptime != EMS_VALUE_LONG_NOTSET) { + myDebug_P(PSTR(" Total UBA working time: %d days %d hours %d minutes"), + EMS_Boiler.UBAuptime / 1440, + (EMS_Boiler.UBAuptime % 1440) / 60, + EMS_Boiler.UBAuptime % 60); } } - // UBAParameterWW - _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); - _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); - if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { - myDebug_P(PSTR(" Warm Water comfort setting: Hot")); - } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { - myDebug_P(PSTR(" Warm Water comfort setting: Eco")); - } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { - myDebug_P(PSTR(" Warm Water comfort setting: Intelligent")); - } - - _renderIntValue("Warm Water selected temperature", "C", EMS_Boiler.wWSelTemp); - _renderIntValue("Warm Water desired temperature", "C", EMS_Boiler.wWDesiredTemp); - - // UBAMonitorWWMessage - _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) { - myDebug_P(PSTR(" Warm Water active time: %d days %d hours %d minutes"), - EMS_Boiler.wWWorkM / 1440, - (EMS_Boiler.wWWorkM % 1440) / 60, - EMS_Boiler.wWWorkM % 60); - } - _renderBoolValue("Warm Water 3-way valve", EMS_Boiler.wWHeat); - - // UBAMonitorFast - _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); - _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); - _renderBoolValue("Ignition", EMS_Boiler.ignWork); - _renderBoolValue("Circulation pump", EMS_Boiler.wWCirc); - _renderIntValue("Burner selected max power", "%", EMS_Boiler.selBurnPow); - _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); - _renderShortValue("Flame current", "uA", EMS_Boiler.flameCurr); - _renderIntValue("System pressure", "bar", EMS_Boiler.sysPress, 10); - if (EMS_Boiler.serviceCode == EMS_VALUE_USHORT_NOTSET) { - myDebug_P(PSTR(" System service code: %s"), EMS_Boiler.serviceCodeChar); - } else { - myDebug_P(PSTR(" System service code: %s (%d)"), EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); - } - - // UBAParametersMessage - _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); - _renderIntValue("Boiler circuit pump modulation max power", "%", EMS_Boiler.pump_mod_max); - _renderIntValue("Boiler circuit pump modulation min power", "%", EMS_Boiler.pump_mod_min); - - // UBAMonitorSlow - if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) { - _renderShortValue("Outside temperature", "C", EMS_Boiler.extTemp); - } - _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) { - myDebug_P(PSTR(" Total burner operating time: %d days %d hours %d minutes"), - EMS_Boiler.burnWorkMin / 1440, - (EMS_Boiler.burnWorkMin % 1440) / 60, - EMS_Boiler.burnWorkMin % 60); - } - if (EMS_Boiler.heatWorkMin != EMS_VALUE_LONG_NOTSET) { - myDebug_P(PSTR(" Total heat operating time: %d days %d hours %d minutes"), - EMS_Boiler.heatWorkMin / 1440, - (EMS_Boiler.heatWorkMin % 1440) / 60, - EMS_Boiler.heatWorkMin % 60); - } - if (EMS_Boiler.UBAuptime != EMS_VALUE_LONG_NOTSET) { - myDebug_P(PSTR(" Total UBA working time: %d days %d hours %d minutes"), - EMS_Boiler.UBAuptime / 1440, - (EMS_Boiler.UBAuptime % 1440) / 60, - EMS_Boiler.UBAuptime % 60); - } - - // For SM10/SM100 Solar Module + // For SM10/SM100/SM200 Solar Module if (ems_getSolarModuleEnabled()) { myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("%sSolar Module stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug_P(PSTR("%sSolar Module data:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug_P(PSTR(" Solar module: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_SOLAR, buffer_type)); - _renderShortValue("Collector temperature", "C", EMS_SolarModule.collectorTemp); - _renderShortValue("Bottom temperature", "C", EMS_SolarModule.bottomTemp); + _renderShortValue("Collector temperature (TS1)", "C", EMS_SolarModule.collectorTemp); + _renderShortValue("Bottom temperature (TS2)", "C", EMS_SolarModule.bottomTemp); + _renderShortValue("Bottom temperature (TS5)", "C", EMS_SolarModule.bottomTemp2); _renderIntValue("Pump modulation", "%", EMS_SolarModule.pumpModulation); - _renderBoolValue("Pump active", EMS_SolarModule.pump); + _renderBoolValue("Valve (VS2) status", EMS_SolarModule.valveStatus); + _renderBoolValue("Pump (PS1) 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 + _renderLongValue("Energy last hour", "Wh", EMS_SolarModule.EnergyLastHour, 1); // *10 + _renderLongValue("Energy today", "Wh", EMS_SolarModule.EnergyToday, 0); + _renderLongValue("Energy total", "kWh", EMS_SolarModule.EnergyTotal, 1); // *10 } // For HeatPumps if (ems_getHeatPumpEnabled()) { myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("%sHeat Pump stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug_P(PSTR("%sHeat Pump data:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug_P(PSTR(" Heat Pump module: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_HEATPUMP, buffer_type)); _renderIntValue("Pump modulation", "%", EMS_HeatPump.HPModulation); _renderIntValue("Pump speed", "%", EMS_HeatPump.HPSpeed); } - // Thermostat stats + // Thermostat data if (ems_getThermostatEnabled()) { myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("%sThermostat stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug_P(PSTR("%sThermostat data:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug_P(PSTR(" Thermostat: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_THERMOSTAT, buffer_type, false)); // Render Thermostat Date & Time - uint8_t model = ems_getThermostatModel(); - if ((model != EMS_DEVICE_FLAG_EASY)) { + uint8_t model = ems_getThermostatFlags(); + if (strlen(EMS_Thermostat.datetime) > 2) { myDebug_P(PSTR(" Thermostat time is %s"), EMS_Thermostat.datetime); } + // settings parameters + if (EMS_Thermostat.ibaMainDisplay != EMS_VALUE_INT_NOTSET) { + if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_INTTEMP) { + myDebug_P(PSTR(" Display: internal temperature")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_INTSETPOINT) { + myDebug_P(PSTR(" Display: internal setpoint")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_EXTTEMP) { + myDebug_P(PSTR(" Display: external temperature")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_BURNERTEMP) { + myDebug_P(PSTR(" Display: burner temperature")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_WWTEMP) { + myDebug_P(PSTR(" Display: WW temperature")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_FUNCMODE) { + myDebug_P(PSTR(" Display: functioning mode")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_TIME) { + myDebug_P(PSTR(" Display: time")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_DATE) { + myDebug_P(PSTR(" Display: date")); + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_SMOKETEMP) { + myDebug_P(PSTR(" Display: smoke temperature")); + } + } + if (EMS_Thermostat.ibaLanguage != EMS_VALUE_INT_NOTSET) { + if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_GERMAN) { + myDebug_P(PSTR(" Language: German")); + } else if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_DUTCH) { + myDebug_P(PSTR(" Language: Dutch")); + } else if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_FRENCH) { + myDebug_P(PSTR(" Language: French")); + } else if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_ITALIAN) { + myDebug_P(PSTR(" Language: Italian")); + } + } + if (EMS_Thermostat.ibaCalIntTemperature != EMS_VALUE_INT_NOTSET) { + _renderIntValue("Offset int. temperature", "K", EMS_Thermostat.ibaCalIntTemperature, 10); // offset int. temperature sensor, by * 0.1 Kelvin + } + if (EMS_Thermostat.ibaMinExtTemperature != EMS_VALUE_SHORT_NOTSET) { + _renderShortValue("Min ext. temperature", "C", EMS_Thermostat.ibaMinExtTemperature, 0); // min ext temp for heating curve, in deg. + } + if (EMS_Thermostat.ibaBuildingType != EMS_VALUE_INT_NOTSET) { + if (EMS_Thermostat.ibaBuildingType == EMS_VALUE_IBASettings_BUILDING_LIGHT) { + myDebug_P(PSTR(" Building: light")); + } else if (EMS_Thermostat.ibaBuildingType == EMS_VALUE_IBASettings_BUILDING_MEDIUM) { + myDebug_P(PSTR(" Building: medium")); + } else if (EMS_Thermostat.ibaBuildingType == EMS_VALUE_IBASettings_BUILDING_HEAVY) { + myDebug_P(PSTR(" Building: heavy")); + } + } + if (EMS_Thermostat.ibaClockOffset != EMS_VALUE_INT_NOTSET) { + _renderIntValue("Offset clock", "s", EMS_Thermostat.ibaClockOffset); // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s + } + uint8_t _m_setpoint, _m_curr; switch (model) { case EMS_DEVICE_FLAG_EASY: _m_setpoint = 10; // *100 _m_curr = 10; // *100 break; - case EMS_DEVICE_FLAG_JUNKERS: + case EMS_DEVICE_FLAG_JUNKERS1: + case EMS_DEVICE_FLAG_JUNKERS2: _m_setpoint = 1; // *10 _m_curr = 1; // *10 break; @@ -428,7 +519,7 @@ void showInfo() { // Render Day/Night/Holiday Temperature on RC35s // there is no single setpoint temp, but one for day, night and vacation - if (model == EMS_DEVICE_FLAG_RC35) { + if ((model == EMS_DEVICE_FLAG_RC35) || (model == EMS_DEVICE_FLAG_RC30N)) { if (EMS_Thermostat.hc[hc_num - 1].summer_mode) { myDebug_P(PSTR(" Program is set to Summer mode")); } else if (EMS_Thermostat.hc[hc_num - 1].holiday_mode) { @@ -440,6 +531,10 @@ void showInfo() { _renderIntValue(" Vacation temperature", "C", EMS_Thermostat.hc[hc_num - 1].holidaytemp, 2); // convert to a single byte * 2 } + // show flow temp if we have it + if (EMS_Thermostat.hc[hc_num - 1].circuitcalctemp != EMS_VALUE_INT_NOTSET) + _renderIntValue(" Calculated flow temperature", "C", EMS_Thermostat.hc[hc_num - 1].circuitcalctemp); + // Render Thermostat Mode _EMS_THERMOSTAT_MODE thermoMode; thermoMode = _getThermostatMode(hc_num); @@ -455,29 +550,55 @@ void showInfo() { myDebug_P(PSTR(" Mode is set to day")); } - // Render Thermostat Day Mode - thermoMode = _getThermostatDayMode(hc_num); - if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { - myDebug_P(PSTR(" Day Mode is set to night")); - } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { - myDebug_P(PSTR(" Day Mode is set to day")); + // Render Thermostat Mode2, only if its in Auto mode + if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { + thermoMode = _getThermostatMode2(hc_num); + if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { + myDebug_P(PSTR(" Mode type is set to night")); + } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { + myDebug_P(PSTR(" Mode type is set to day")); + } else if (thermoMode == EMS_THERMOSTAT_MODE_COMFORT) { + myDebug_P(PSTR(" Mode type is set to comfort")); + } else if (thermoMode == EMS_THERMOSTAT_MODE_ECO) { + myDebug_P(PSTR(" Mode type is set to eco")); + } } } } } // Mixing modules sensors - if (ems_getMixingDeviceEnabled()) { + if (ems_getMixingModuleEnabled()) { myDebug_P(PSTR("")); // newline - myDebug_P(PSTR("%sMixing module stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); - _renderUShortValue("Switch temperature", "C", EMS_Boiler.switchTemp); + myDebug_P(PSTR("%sMixing module data:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug_P(PSTR(" Mixing Module: %s"), ems_getDeviceDescription(EMS_DEVICE_TYPE_MIXING, buffer_type, false)); + if ((EMS_Boiler.switchTemp != EMS_VALUE_USHORT_NOTSET) && (EMS_Boiler.switchTemp != 0)) { + _renderUShortValue("Switch temperature", "C", EMS_Boiler.switchTemp); + } - for (uint8_t hc_num = 1; hc_num <= EMS_THERMOSTAT_MAXHC; hc_num++) { - if (EMS_Mixing.hc[hc_num - 1].active) { + for (uint8_t hc_num = 1; hc_num <= EMS_MIXING_MAXHC; hc_num++) { + if (EMS_MixingModule.hc[hc_num - 1].active) { myDebug_P(PSTR(" Mixing Circuit %d"), hc_num); - _renderUShortValue(" Current flow temperature", "C", EMS_Mixing.hc[hc_num - 1].flowTemp); - _renderIntValue(" Current pump modulation", "%", EMS_Mixing.hc[hc_num - 1].pumpMod); - _renderIntValue(" Current valve status", "%", EMS_Mixing.hc[hc_num - 1].valveStatus); + if (EMS_MixingModule.hc[hc_num - 1].flowTemp != EMS_VALUE_USHORT_NOTSET) + _renderUShortValue(" Current flow temperature", "C", EMS_MixingModule.hc[hc_num - 1].flowTemp); + if (EMS_MixingModule.hc[hc_num - 1].flowSetTemp != EMS_VALUE_INT_NOTSET) + _renderIntValue(" Setpoint flow temperature", "C", EMS_MixingModule.hc[hc_num - 1].flowSetTemp); + if (EMS_MixingModule.hc[hc_num - 1].pumpMod != EMS_VALUE_INT_NOTSET) + _renderIntValue(" Current pump modulation", "%", EMS_MixingModule.hc[hc_num - 1].pumpMod); + if (EMS_MixingModule.hc[hc_num - 1].valveStatus != EMS_VALUE_INT_NOTSET) + _renderIntValue(" Current valve status", "", EMS_MixingModule.hc[hc_num - 1].valveStatus); + } + } + + for (uint8_t wwc_num = 1; wwc_num <= EMS_MIXING_MAXWWC; wwc_num++) { + if (EMS_MixingModule.wwc[wwc_num - 1].active) { + myDebug_P(PSTR(" Warm Water Circuit %d"), wwc_num); + if (EMS_MixingModule.wwc[wwc_num - 1].flowTemp != EMS_VALUE_USHORT_NOTSET) + _renderUShortValue(" Current warm water temperature", "C", EMS_MixingModule.wwc[wwc_num - 1].flowTemp); + if (EMS_MixingModule.wwc[wwc_num - 1].pumpMod != EMS_VALUE_INT_NOTSET) + _renderIntValue(" Current pump status", "", EMS_MixingModule.wwc[wwc_num - 1].pumpMod); + if (EMS_MixingModule.wwc[wwc_num - 1].tempStatus != EMS_VALUE_INT_NOTSET) + _renderIntValue(" Current temp status", "", EMS_MixingModule.wwc[wwc_num - 1].tempStatus); } } } @@ -485,348 +606,661 @@ void showInfo() { // Dallas external temp sensors if (EMSESP_Settings.dallas_sensors) { myDebug_P(PSTR("")); // newline - char buffer[128] = {0}; - char valuestr[8] = {0}; // for formatting temp + char buffer[128] = {0}; + char buffer2[128] = {0}; + char valuestr[8] = {0}; // for formatting temp myDebug_P(PSTR("%sExternal temperature sensors:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); for (uint8_t i = 0; i < EMSESP_Settings.dallas_sensors; i++) { - myDebug_P(PSTR(" Sensor #%d %s: %s C"), i + 1, ds18.getDeviceString(buffer, i), _float_to_char(valuestr, ds18.getValue(i))); + float sensorValue = ds18.getValue(i); + if (sensorValue != DS18_DISCONNECTED) { + myDebug_P(PSTR(" Sensor #%d type: %s id: %s temperature: %s C"), + i + 1, + ds18.getDeviceType(buffer, i), + ds18.getDeviceID(buffer2, i), + _float_to_char(valuestr, sensorValue)); + } } } myDebug_P(PSTR("")); // newline } +// scan for external Dallas sensors and display +void scanDallas() { + EMSESP_Settings.dallas_sensors = ds18.scan(); + if (EMSESP_Settings.dallas_sensors) { + char buffer[128]; + char buffer2[128]; + for (uint8_t i = 0; i < EMSESP_Settings.dallas_sensors; i++) { + myDebug_P(PSTR("External temperature sensor found, type: %s id: %s"), ds18.getDeviceType(buffer, i), ds18.getDeviceID(buffer2, i)); + } + } +} + // send all dallas sensor values as a JSON package to MQTT -void publishSensorValues() { - // don't send if MQTT is connected - if (!myESP.isMQTTConnected()) { - return; +bool publishSensorValues() { + // don't send if MQTT is not connected + if (!myESP.isMQTTConnected() || (EMSESP_Settings.publish_time == -1)) { + return false; } if (!EMSESP_Settings.dallas_sensors) { - return; // no sensors attached + return false; // no sensors attached } - StaticJsonDocument<200> doc; - JsonObject sensors = doc.to(); + // each payload per sensor is 30 bytes so calculate if we have enough space + if ((EMSESP_Settings.dallas_sensors * 50) > DS18_MQTT_PAYLOAD_MAXSIZE) { + myDebug_P(PSTR("Error: too many Dallas sensors for MQTT payload")); + } - bool hasdata = false; - char label[8] = {0}; + StaticJsonDocument doc; + JsonObject sensors = doc.to(); - // see if the sensor values have changed, if so send it on + bool hasdata = false; + char buffer[128]; // temp string buffer + + // if we're not using nested JSON, send each sensor out seperately + if (!myESP.mqttUseNestedJson()) { + for (uint8_t i = 0; i < EMSESP_Settings.dallas_sensors; i++) { + float sensorValue = ds18.getValue(i); + if (sensorValue != DS18_DISCONNECTED) { + hasdata = true; + char topic[30]; // sensors{1-n} + strlcpy(topic, TOPIC_EXTERNAL_SENSORS, sizeof(topic)); // create topic + strlcat(topic, _int_to_char(buffer, i + 1), sizeof(topic)); + sensors[PAYLOAD_EXTERNAL_SENSOR_ID] = ds18.getDeviceID(buffer, i); // add ID + sensors[PAYLOAD_EXTERNAL_SENSOR_TEMP] = sensorValue; // add temp value + myESP.mqttPublish(topic, doc); // and publish + } + } + if (hasdata) { + myDebugLog("Publishing external sensor data via MQTT"); + } + return true; // exit + } + + // group all sensors together - https://github.com/proddy/EMS-ESP/issues/327 for (uint8_t i = 0; i < EMSESP_Settings.dallas_sensors; i++) { - // round to 2 decimal places. from https://arduinojson.org/v6/faq/how-to-configure-the-serialization-of-floats/ - float sensorValue = (int)(ds18.getValue(i) * 100 + 0.5) / 100.0; - if (sensorValue != DS18_DISCONNECTED && sensorValue != DS18_CRC_ERROR) { - sprintf(label, PAYLOAD_EXTERNAL_SENSORS, (i + 1)); - sensors[label] = sensorValue; - hasdata = true; + float sensorValue = ds18.getValue(i); + if (sensorValue != DS18_DISCONNECTED) { + hasdata = true; +#ifdef SENSOR_MQTT_USEID + sensors[ds18.getDeviceID(buffer, i)] = sensorValue; +#else + char sensorID[10]; // sensor{1-n} + strlcpy(sensorID, PAYLOAD_EXTERNAL_SENSOR_NUM, sizeof(sensorID)); + strlcat(sensorID, _int_to_char(buffer, i + 1), sizeof(sensorID)); + JsonObject dataSensor = sensors.createNestedObject(sensorID); + dataSensor[PAYLOAD_EXTERNAL_SENSOR_ID] = ds18.getDeviceID(buffer, i); + dataSensor[PAYLOAD_EXTERNAL_SENSOR_TEMP] = sensorValue; +#endif } } - if (!hasdata) { - return; // nothing to send + if (hasdata) { + myDebugLog("Publishing external sensor data via MQTT"); + return (myESP.mqttPublish(TOPIC_EXTERNAL_SENSORS, doc)); } - char data[200] = {0}; - serializeJson(doc, data, sizeof(data)); - myDebugLog("Publishing external sensor data via MQTT"); - myESP.mqttPublish(TOPIC_EXTERNAL_SENSORS, data); + return false; +} + +// publish Boiler data via MQTT +bool publishEMSValues_boiler() { + const size_t capacity = JSON_OBJECT_SIZE(47); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/ + DynamicJsonDocument doc(capacity); + JsonObject rootBoiler = doc.to(); + + char s[20]; // for formatting strings + + if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { + rootBoiler["wWComfort"] = "Hot"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { + rootBoiler["wWComfort"] = "Eco"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { + rootBoiler["wWComfort"] = "Intelligent"; + } + + if (EMS_Boiler.wWSelTemp != EMS_VALUE_INT_NOTSET) { + rootBoiler["wWSelTemp"] = EMS_Boiler.wWSelTemp; + } + if (EMS_Boiler.wWDesinfectTemp != EMS_VALUE_INT_NOTSET) { + rootBoiler["wWDesinfectionTemp"] = EMS_Boiler.wWDesinfectTemp; + } + if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET) { + rootBoiler["selFlowTemp"] = EMS_Boiler.selFlowTemp; + } + if (EMS_Boiler.selBurnPow != EMS_VALUE_INT_NOTSET) { + rootBoiler["selBurnPow"] = EMS_Boiler.selBurnPow; + } + if (EMS_Boiler.curBurnPow != EMS_VALUE_INT_NOTSET) { + rootBoiler["curBurnPow"] = EMS_Boiler.curBurnPow; + } + if (EMS_Boiler.pumpMod != EMS_VALUE_INT_NOTSET) { + rootBoiler["pumpMod"] = EMS_Boiler.pumpMod; + } + if (EMS_Boiler.wWCircPump != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWCircPump"] = EMS_Boiler.wWCircPump; + } + if (EMS_Boiler.wWCircPumpType != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWCiPuType"] = EMS_Boiler.wWCircPumpType; + } + if (EMS_Boiler.wWCircPumpMode != EMS_VALUE_INT_NOTSET) { + rootBoiler["wWCiPuMode"] = EMS_Boiler.wWCircPumpMode; + } + if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) { + rootBoiler["outdoorTemp"] = (float)EMS_Boiler.extTemp / 10; + } + if (EMS_Boiler.wWCurTmp != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["wWCurTmp"] = (float)EMS_Boiler.wWCurTmp / 10; + } + if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET) { + rootBoiler["wWCurFlow"] = (float)EMS_Boiler.wWCurFlow / 10; + } + if (EMS_Boiler.curFlowTemp != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["curFlowTemp"] = (float)EMS_Boiler.curFlowTemp / 10; + } + if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["retTemp"] = (float)EMS_Boiler.retTemp / 10; + } + if (EMS_Boiler.switchTemp != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["switchTemp"] = (float)EMS_Boiler.switchTemp / 10; + } + if (EMS_Boiler.sysPress != EMS_VALUE_INT_NOTSET) { + rootBoiler["sysPress"] = (float)EMS_Boiler.sysPress / 10; + } + if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["boilTemp"] = (float)EMS_Boiler.boilTemp / 10; + } + if (EMS_Boiler.wwStorageTemp1 != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["wwStorageTemp1"] = (float)EMS_Boiler.wwStorageTemp1 / 10; + } + if (EMS_Boiler.wwStorageTemp2 != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["wwStorageTemp2"] = (float)EMS_Boiler.wwStorageTemp2 / 10; + } + if (EMS_Boiler.exhaustTemp != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["exhaustTemp"] = (float)EMS_Boiler.exhaustTemp / 10; + } + if (EMS_Boiler.wWActivated != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); + } + if (EMS_Boiler.wWOneTime != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWOnetime"] = _bool_to_char(s, EMS_Boiler.wWOneTime); + } + if (EMS_Boiler.wWDesinfecting != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWDesinfecting"] = _bool_to_char(s, EMS_Boiler.wWDesinfecting); + } + if (EMS_Boiler.wWReadiness != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWReady"] = _bool_to_char(s, EMS_Boiler.wWReadiness); + } + if (EMS_Boiler.wWRecharging != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWRecharge"] = _bool_to_char(s, EMS_Boiler.wWRecharging); + } + if (EMS_Boiler.wWTemperatureOK != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWTempOK"] = _bool_to_char(s, EMS_Boiler.wWTemperatureOK); + } + if (EMS_Boiler.wWCirc != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); + } + if (EMS_Boiler.burnGas != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); + } + if (EMS_Boiler.flameCurr != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["flameCurr"] = (float)(int16_t)EMS_Boiler.flameCurr / 10; + } + if (EMS_Boiler.heatPmp != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); + } + if (EMS_Boiler.fanWork != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); + } + if (EMS_Boiler.ignWork != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); + } + if (EMS_Boiler.heating_temp != EMS_VALUE_INT_NOTSET) { + rootBoiler["heating_temp"] = EMS_Boiler.heating_temp; + } + if (EMS_Boiler.pump_mod_max != EMS_VALUE_INT_NOTSET) { + rootBoiler["pump_mod_max"] = EMS_Boiler.pump_mod_max; + } + if (EMS_Boiler.pump_mod_min != EMS_VALUE_INT_NOTSET) { + rootBoiler["pump_mod_min"] = EMS_Boiler.pump_mod_min; + } + if (EMS_Boiler.wWHeat != EMS_VALUE_BOOL_NOTSET) { + rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); + } + if (EMS_Boiler.wWStarts != EMS_VALUE_LONG_NOTSET) { + rootBoiler["wWStarts"] = EMS_Boiler.wWStarts; + } + if (EMS_Boiler.wWWorkM != EMS_VALUE_LONG_NOTSET) { + rootBoiler["wWWorkM"] = EMS_Boiler.wWWorkM; + } + if (EMS_Boiler.UBAuptime != EMS_VALUE_LONG_NOTSET) { + rootBoiler["UBAuptime"] = EMS_Boiler.UBAuptime; + } + if (EMS_Boiler.burnStarts != EMS_VALUE_LONG_NOTSET) { + rootBoiler["burnStarts"] = EMS_Boiler.burnStarts; + } + if (EMS_Boiler.burnWorkMin != EMS_VALUE_LONG_NOTSET) { + rootBoiler["burnWorkMin"] = EMS_Boiler.burnWorkMin; + } + if (EMS_Boiler.heatWorkMin != EMS_VALUE_LONG_NOTSET) { + rootBoiler["heatWorkMin"] = EMS_Boiler.heatWorkMin; + } + + if (EMS_Boiler.serviceCode != EMS_VALUE_USHORT_NOTSET) { + rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; + } + + // see if the heating or hot tap water has changed, if so send + // last_boilerActive stores heating in bit 1 and tap water in bit 2 + static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off + if (last_boilerActive != ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive)) { + myESP.mqttPublish(TOPIC_BOILER_TAPWATER_ACTIVE, EMS_Boiler.tapwaterActive ? "1" : "0"); + myESP.mqttPublish(TOPIC_BOILER_HEATING_ACTIVE, EMS_Boiler.heatingActive ? "1" : "0"); + last_boilerActive = ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive); // remember last state + } + + return (myESP.mqttPublish(TOPIC_BOILER_DATA, doc)); +} + +// handle the thermostat values +bool publishEMSValues_thermostat() { + StaticJsonDocument doc; + JsonObject rootThermostat = doc.to(); + JsonObject dataThermostat; + + bool has_data = false; + + for (uint8_t hc_v = 1; hc_v <= EMS_THERMOSTAT_MAXHC; hc_v++) { + _EMS_Thermostat_HC * thermostat = &EMS_Thermostat.hc[hc_v - 1]; + + // only send if we have an active Heating Circuit with an actual setpoint temp temperature values + if ((thermostat->active) && (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET)) { + uint8_t model = ems_getThermostatFlags(); // fetch model flags + has_data = true; + + if (myESP.mqttUseNestedJson()) { + // create nested json for each HC + char hc[10]; // hc{1-4} + strlcpy(hc, THERMOSTAT_HC, sizeof(hc)); + char s[20]; // for formatting strings + strlcat(hc, _int_to_char(s, thermostat->hc), sizeof(hc)); + dataThermostat = rootThermostat.createNestedObject(hc); + } else { + dataThermostat = rootThermostat; + } + + // different logic depending on thermostat types + if (model == EMS_DEVICE_FLAG_EASY) { + if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) + dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 100; + if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) + dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 100; + } else if ((model == EMS_DEVICE_FLAG_JUNKERS1) || (model == EMS_DEVICE_FLAG_JUNKERS2)) { + if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) + dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 10; + if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) + dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 10; + } else { + if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) + dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 2; + if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) + dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 10; + + if (thermostat->daytemp != EMS_VALUE_INT_NOTSET) + dataThermostat[THERMOSTAT_DAYTEMP] = (float)thermostat->daytemp / 2; + if (thermostat->nighttemp != EMS_VALUE_INT_NOTSET) + dataThermostat[THERMOSTAT_NIGHTTEMP] = (float)thermostat->nighttemp / 2; + if (thermostat->holidaytemp != EMS_VALUE_INT_NOTSET) + dataThermostat[THERMOSTAT_HOLIDAYTEMP] = (float)thermostat->holidaytemp / 2; + + if (thermostat->heatingtype != EMS_VALUE_INT_NOTSET) + dataThermostat[THERMOSTAT_HEATINGTYPE] = thermostat->heatingtype; + + if (thermostat->circuitcalctemp != EMS_VALUE_INT_NOTSET) + dataThermostat[THERMOSTAT_CIRCUITCALCTEMP] = thermostat->circuitcalctemp; + } + + // Thermostat Mode + _EMS_THERMOSTAT_MODE thermoMode = _getThermostatMode(hc_v); + if (thermoMode == EMS_THERMOSTAT_MODE_OFF) { + dataThermostat[THERMOSTAT_MODE] = "off"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_MANUAL) { + dataThermostat[THERMOSTAT_MODE] = "manual"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { + dataThermostat[THERMOSTAT_MODE] = "auto"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { + dataThermostat[THERMOSTAT_MODE] = "day"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { + dataThermostat[THERMOSTAT_MODE] = "night"; + } + // Render Thermostat Mode2, only if its in Auto mode + if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { + thermoMode = _getThermostatMode2(hc_v); + if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { + dataThermostat[THERMOSTAT_MODETYPE] = "night"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { + dataThermostat[THERMOSTAT_MODETYPE] = "day"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_COMFORT) { + dataThermostat[THERMOSTAT_MODETYPE] = "comfort"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_ECO) { + dataThermostat[THERMOSTAT_MODETYPE] = "eco"; + } + } + + // if its not nested, send immediately + if (!myESP.mqttUseNestedJson()) { + char topic[30]; + char s[20]; // for formatting strings + strlcpy(topic, TOPIC_THERMOSTAT_DATA, sizeof(topic)); + strlcat(topic, _int_to_char(s, thermostat->hc), sizeof(topic)); // append hc to topic + char data[MYESP_JSON_MAXSIZE_MEDIUM]; + serializeJson(doc, data); + myESP.mqttPublish(topic, data); + } + } + } + + // if we're using nested json, send all in one go + if (myESP.mqttUseNestedJson() && has_data) { + char data[MYESP_JSON_MAXSIZE_MEDIUM]; + serializeJson(doc, data); + myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); + } + + return (has_data); +} + +// publish Settings parameters via MQTT +bool publishEMSValues_settings() { + const size_t capacity = JSON_OBJECT_SIZE(6); // must recalculate if more objects added https://arduinojson.org/v6/assistant/ + DynamicJsonDocument doc(capacity); + JsonObject rootSettings = doc.to(); + + if (EMS_Thermostat.ibaMainDisplay != EMS_VALUE_INT_NOTSET) { + if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_INTTEMP) { + rootSettings["display"] = "int. temperature"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_INTSETPOINT) { + rootSettings["display"] = "int. setpoint"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_EXTTEMP) { + rootSettings["display"] = "ext. temperature"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_BURNERTEMP) { + rootSettings["display"] = "burner temperature"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_WWTEMP) { + rootSettings["display"] = "WW temperature"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_FUNCMODE) { + rootSettings["display"] = "functioning mode"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_TIME) { + rootSettings["display"] = "time"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_DATE) { + rootSettings["display"] = "date"; + } else if (EMS_Thermostat.ibaMainDisplay == EMS_VALUE_IBASettings_DISPLAY_SMOKETEMP) { + rootSettings["display"] = "smoke temperature"; + } + } + if (EMS_Thermostat.ibaLanguage != EMS_VALUE_INT_NOTSET) { + if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_GERMAN) { + rootSettings["language"] = "German"; + } else if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_DUTCH) { + rootSettings["language"] = "Dutch"; + } else if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_FRENCH) { + rootSettings["language"] = "French"; + } else if (EMS_Thermostat.ibaLanguage == EMS_VALUE_IBASettings_LANG_ITALIAN) { + rootSettings["language"] = "Italian"; + } + } + if (EMS_Thermostat.ibaCalIntTemperature != EMS_VALUE_INT_NOTSET) { + rootSettings["CalIntTemperature"] = (float)EMS_Thermostat.ibaCalIntTemperature / 10; // offset int. temperature sensor, by * 0.1 Kelvin + } + if (EMS_Thermostat.ibaMinExtTemperature != EMS_VALUE_SHORT_NOTSET) { + rootSettings["MinExtTemperature"] = EMS_Thermostat.ibaMinExtTemperature; // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 + } + if (EMS_Thermostat.ibaBuildingType != EMS_VALUE_INT_NOTSET) { + if (EMS_Thermostat.ibaBuildingType == EMS_VALUE_IBASettings_BUILDING_LIGHT) { + rootSettings["building"] = "light"; + } else if (EMS_Thermostat.ibaBuildingType == EMS_VALUE_IBASettings_BUILDING_MEDIUM) { + rootSettings["building"] = "medium"; + } else if (EMS_Thermostat.ibaBuildingType == EMS_VALUE_IBASettings_BUILDING_HEAVY) { + rootSettings["building"] = "heavy"; + } + } + if (EMS_Thermostat.ibaClockOffset != EMS_VALUE_INT_NOTSET) { + rootSettings["clockOffset"] = EMS_Thermostat.ibaClockOffset; // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s + } + return (myESP.mqttPublish(TOPIC_SETTINGS_DATA, doc)); +} + +// publish mixing data +// only sending if we have an active hc +bool publishEMSValues_mixing() { + char s[20]; // for formatting strings + StaticJsonDocument doc; + JsonObject rootMixing = doc.to(); + bool has_data = false; + + for (uint8_t hc_v = 1; hc_v <= EMS_MIXING_MAXHC; hc_v++) { + _EMS_MixingModule_HC * mixingHC = &EMS_MixingModule.hc[hc_v - 1]; + + if (mixingHC->active) { + has_data = true; + char hc[10]; // hc{1-4} + strlcpy(hc, MIXING_HC, sizeof(hc)); + strlcat(hc, _int_to_char(s, mixingHC->hc), sizeof(hc)); + JsonObject dataMixingHC = rootMixing.createNestedObject(hc); + if (mixingHC->flowTemp != EMS_VALUE_USHORT_NOTSET) + dataMixingHC["flowTemp"] = (float)mixingHC->flowTemp / 10; + if (mixingHC->flowSetTemp != EMS_VALUE_INT_NOTSET) + dataMixingHC["setflowTemp"] = mixingHC->flowSetTemp; + if (mixingHC->pumpMod != EMS_VALUE_INT_NOTSET) + dataMixingHC["pumpMod"] = mixingHC->pumpMod; + if (mixingHC->valveStatus != EMS_VALUE_INT_NOTSET) + dataMixingHC["valveStatus"] = mixingHC->valveStatus; + } + } + + for (uint8_t wwc_v = 1; wwc_v <= EMS_MIXING_MAXWWC; wwc_v++) { + _EMS_MixingModule_WWC * mixingWWC = &EMS_MixingModule.wwc[wwc_v - 1]; + + if (mixingWWC->active) { + has_data = true; + char wwc[10]; // wwc{1-2} + strlcpy(wwc, MIXING_WWC, sizeof(wwc)); + strlcat(wwc, _int_to_char(s, mixingWWC->wwc), sizeof(wwc)); + JsonObject dataMixing = rootMixing.createNestedObject(wwc); + if (mixingWWC->flowTemp != EMS_VALUE_USHORT_NOTSET) + dataMixing["wwTemp"] = (float)mixingWWC->flowTemp / 10; + if (mixingWWC->pumpMod != EMS_VALUE_INT_NOTSET) + dataMixing["pumpStatus"] = mixingWWC->pumpMod; + if (mixingWWC->tempStatus != EMS_VALUE_INT_NOTSET) + dataMixing["tempStatus"] = mixingWWC->tempStatus; + } + } + + if (has_data) { + myESP.mqttPublish(TOPIC_MIXING_DATA, doc); + return true; + } + + return false; +} + +// For SM10 and SM100/SM200 Solar Modules +bool publishEMSValues_solar() { + StaticJsonDocument doc; + JsonObject rootSM = doc.to(); + + if (EMS_SolarModule.collectorTemp != EMS_VALUE_SHORT_NOTSET) { + rootSM[SM_COLLECTORTEMP] = (float)EMS_SolarModule.collectorTemp / 10; + } + if (EMS_SolarModule.bottomTemp != EMS_VALUE_SHORT_NOTSET) { + rootSM[SM_BOTTOMTEMP] = (float)EMS_SolarModule.bottomTemp / 10; + } + if (EMS_SolarModule.bottomTemp2 != EMS_VALUE_SHORT_NOTSET) { + rootSM[SM_BOTTOMTEMP2] = (float)EMS_SolarModule.bottomTemp2 / 10; + } + if (EMS_SolarModule.pumpModulation != EMS_VALUE_INT_NOTSET) { + rootSM[SM_PUMPMODULATION] = EMS_SolarModule.pumpModulation; + } + if (EMS_SolarModule.pump != EMS_VALUE_BOOL_NOTSET) { + char s[20]; + rootSM[SM_PUMP] = _bool_to_char(s, EMS_SolarModule.pump); + } + if (EMS_SolarModule.valveStatus != EMS_VALUE_BOOL_NOTSET) { + char s[20]; + rootSM[SM_VALVESTATUS] = _bool_to_char(s, EMS_SolarModule.valveStatus); + } + if (EMS_SolarModule.pumpWorkMin != EMS_VALUE_LONG_NOTSET) { + rootSM[SM_PUMPWORKMIN] = (float)EMS_SolarModule.pumpWorkMin; + } + if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_LONG_NOTSET) { + rootSM[SM_ENERGYLASTHOUR] = (float)EMS_SolarModule.EnergyLastHour / 10; + } + if (EMS_SolarModule.EnergyToday != EMS_VALUE_LONG_NOTSET) { + rootSM[SM_ENERGYTODAY] = EMS_SolarModule.EnergyToday; + } + if (EMS_SolarModule.EnergyTotal != EMS_VALUE_LONG_NOTSET) { + rootSM[SM_ENERGYTOTAL] = (float)EMS_SolarModule.EnergyTotal / 10; + } + + return (myESP.mqttPublish(TOPIC_SM_DATA, doc)); +} + +// handle Heat Pump +// returns true if added to MQTT queue went ok +bool publishEMSValues_heatpump() { + StaticJsonDocument doc; + JsonObject rootHP = doc.to(); + + if (EMS_HeatPump.HPModulation != EMS_VALUE_INT_NOTSET) { + rootHP[HP_PUMPMODULATION] = EMS_HeatPump.HPModulation; + } + if (EMS_HeatPump.HPSpeed != EMS_VALUE_INT_NOTSET) { + rootHP[HP_PUMPSPEED] = EMS_HeatPump.HPSpeed; + } + + myDebugLog("Publishing heat pump data via MQTT"); + return (myESP.mqttPublish(TOPIC_HP_DATA, doc)); +} + +// Publish shower data +// returns true if added to MQTT queue went ok +bool do_publishShowerData() { + StaticJsonDocument doc; + JsonObject rootShower = doc.to(); + rootShower[TOPIC_SHOWER_TIMER] = EMSESP_Settings.shower_timer ? "1" : "0"; + rootShower[TOPIC_SHOWER_ALERT] = EMSESP_Settings.shower_alert ? "1" : "0"; + + // only publish shower duration if there is a value + char s[50] = {0}; + if (EMSESP_Shower.duration > SHOWER_MIN_DURATION) { + char buffer[16] = {0}; + strlcpy(s, itoa((uint8_t)((EMSESP_Shower.duration / (1000 * 60)) % 60), buffer, 10), sizeof(s)); + strlcat(s, " minutes and ", sizeof(s)); + strlcat(s, itoa((uint8_t)((EMSESP_Shower.duration / 1000) % 60), buffer, 10), sizeof(s)); + strlcat(s, " seconds", sizeof(s)); + rootShower[TOPIC_SHOWER_DURATION] = s; + } + + myDebugLog("Publishing shower data via MQTT"); + return (myESP.mqttPublish(TOPIC_SHOWER_DATA, doc, false)); // Publish MQTT forcing retain to be off } // send values via MQTT // a json object is created for each device type -void publishEMSValues(bool force) { +bool publishEMSValues(bool force) { // don't send if MQTT is not connected or EMS bus is not connected - if (!myESP.isMQTTConnected() || (!ems_getBusConnected())) { - return; + if (!myESP.isMQTTConnected() || (!ems_getBusConnected()) || (EMSESP_Settings.publish_time == -1)) { + return false; } - char s[20] = {0}; // for formatting strings - StaticJsonDocument doc; - char data[MQTT_MAX_PAYLOAD_SIZE] = {0}; + bool thermo = false; + bool boiler = false; + bool settings = false; + bool mixing = false; + bool solar = false; + bool heatpump = false; - static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off - - // do we have boiler changes? - if (ems_getBoilerEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_BOILER) || force)) { - JsonObject rootBoiler = doc.to(); - - if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { - rootBoiler["wWComfort"] = "Hot"; - } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { - rootBoiler["wWComfort"] = "Eco"; - } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { - rootBoiler["wWComfort"] = "Intelligent"; - } - - if (EMS_Boiler.wWSelTemp != EMS_VALUE_INT_NOTSET) - rootBoiler["wWSelTemp"] = EMS_Boiler.wWSelTemp; - if (EMS_Boiler.wWDesiredTemp != EMS_VALUE_INT_NOTSET) - rootBoiler["wWDesiredTemp"] = EMS_Boiler.wWDesiredTemp; - if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET) - rootBoiler["selFlowTemp"] = EMS_Boiler.selFlowTemp; - if (EMS_Boiler.selBurnPow != EMS_VALUE_INT_NOTSET) - rootBoiler["selBurnPow"] = EMS_Boiler.selBurnPow; - if (EMS_Boiler.curBurnPow != EMS_VALUE_INT_NOTSET) - rootBoiler["curBurnPow"] = EMS_Boiler.curBurnPow; - if (EMS_Boiler.pumpMod != EMS_VALUE_INT_NOTSET) - rootBoiler["pumpMod"] = EMS_Boiler.pumpMod; - if (EMS_Boiler.wWCircPump != EMS_VALUE_BOOL_NOTSET) - rootBoiler["wWCircPump"] = EMS_Boiler.wWCircPump; - - if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) - rootBoiler["outdoorTemp"] = (float)EMS_Boiler.extTemp / 10; - if (EMS_Boiler.wWCurTmp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["wWCurTmp"] = (float)EMS_Boiler.wWCurTmp / 10; - if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET) - rootBoiler["wWCurFlow"] = (float)EMS_Boiler.wWCurFlow / 10; - if (EMS_Boiler.curFlowTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["curFlowTemp"] = (float)EMS_Boiler.curFlowTemp / 10; - if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["retTemp"] = (float)EMS_Boiler.retTemp / 10; - if (EMS_Boiler.switchTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["switchTemp"] = (float)EMS_Boiler.switchTemp / 10; - if (EMS_Boiler.sysPress != EMS_VALUE_INT_NOTSET) - rootBoiler["sysPress"] = (float)EMS_Boiler.sysPress / 10; - if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) - rootBoiler["boilTemp"] = (float)EMS_Boiler.boilTemp / 10; - - if (EMS_Boiler.wWActivated != EMS_VALUE_BOOL_NOTSET) - rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); - - if (EMS_Boiler.wWActivated != EMS_VALUE_BOOL_NOTSET) - rootBoiler["wWOnetime"] = _bool_to_char(s, EMS_Boiler.wWOneTime); - - if (EMS_Boiler.burnGas != EMS_VALUE_BOOL_NOTSET) - rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); - - if (EMS_Boiler.flameCurr != EMS_VALUE_USHORT_NOTSET) - rootBoiler["flameCurr"] = (float)(int16_t)EMS_Boiler.flameCurr / 10; - - if (EMS_Boiler.heatPmp != EMS_VALUE_BOOL_NOTSET) - rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); - - if (EMS_Boiler.fanWork != EMS_VALUE_BOOL_NOTSET) - rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); - - if (EMS_Boiler.ignWork != EMS_VALUE_BOOL_NOTSET) - rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); - - if (EMS_Boiler.wWCirc != EMS_VALUE_BOOL_NOTSET) - rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); - - if (EMS_Boiler.heating_temp != EMS_VALUE_INT_NOTSET) - rootBoiler["heating_temp"] = EMS_Boiler.heating_temp; - if (EMS_Boiler.pump_mod_max != EMS_VALUE_INT_NOTSET) - rootBoiler["pump_mod_max"] = EMS_Boiler.pump_mod_max; - if (EMS_Boiler.pump_mod_min != EMS_VALUE_INT_NOTSET) - rootBoiler["pump_mod_min"] = EMS_Boiler.pump_mod_min; - - if (EMS_Boiler.wWHeat != EMS_VALUE_BOOL_NOTSET) - rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); - - if (abs(EMS_Boiler.wWStarts) != EMS_VALUE_LONG_NOTSET) - rootBoiler["wWStarts"] = (float)EMS_Boiler.wWStarts; - if (abs(EMS_Boiler.wWWorkM) != EMS_VALUE_LONG_NOTSET) - rootBoiler["wWWorkM"] = (float)EMS_Boiler.wWWorkM; - if (abs(EMS_Boiler.UBAuptime) != EMS_VALUE_LONG_NOTSET) - rootBoiler["UBAuptime"] = (float)EMS_Boiler.UBAuptime; - - if (abs(EMS_Boiler.burnStarts) != EMS_VALUE_LONG_NOTSET) - rootBoiler["burnStarts"] = (float)EMS_Boiler.burnStarts; - if (abs(EMS_Boiler.burnWorkMin) != EMS_VALUE_LONG_NOTSET) - rootBoiler["burnWorkMin"] = (float)EMS_Boiler.burnWorkMin; - if (abs(EMS_Boiler.heatWorkMin) != EMS_VALUE_LONG_NOTSET) - rootBoiler["heatWorkMin"] = (float)EMS_Boiler.heatWorkMin; - - if (EMS_Boiler.serviceCode != EMS_VALUE_USHORT_NOTSET) { - rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; - rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; - } - - serializeJson(doc, data, sizeof(data)); - myDebugLog("Publishing boiler data via MQTT"); - myESP.mqttPublish(TOPIC_BOILER_DATA, data); - - // see if the heating or hot tap water has changed, if so send - // last_boilerActive stores heating in bit 1 and tap water in bit 2 - if ((last_boilerActive != ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive)) || force) { - myDebugLog("Publishing hot water and heating states via MQTT"); - myESP.mqttPublish(TOPIC_BOILER_TAPWATER_ACTIVE, EMS_Boiler.tapwaterActive == 1 ? "1" : "0"); - myESP.mqttPublish(TOPIC_BOILER_HEATING_ACTIVE, EMS_Boiler.heatingActive == 1 ? "1" : "0"); - - last_boilerActive = ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive); // remember last state - } - - ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_BOILER); // unset flag - } - - // handle the thermostat values if (ems_getThermostatEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_THERMOSTAT) || force)) { - doc.clear(); - JsonObject rootThermostat = doc.to(); - - for (uint8_t hc_v = 1; hc_v <= EMS_THERMOSTAT_MAXHC; hc_v++) { - _EMS_Thermostat_HC * thermostat = &EMS_Thermostat.hc[hc_v - 1]; - - // only send if we have an active Heating Circuit with real data - if (thermostat->active) { - // build new json object - char hc[10]; // hc{1-4} - strlcpy(hc, THERMOSTAT_HC, sizeof(hc)); - strlcat(hc, _int_to_char(s, thermostat->hc), sizeof(hc)); - JsonObject dataThermostat = rootThermostat.createNestedObject(hc); - uint8_t model = ems_getThermostatModel(); - - // different logic depending on thermostat types - if (model == EMS_DEVICE_FLAG_EASY) { - if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 100; - if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 100; - } else if (model == EMS_DEVICE_FLAG_JUNKERS) { - if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 10; - if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 10; - } else { - if (thermostat->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_SELTEMP] = (float)thermostat->setpoint_roomTemp / 2; - if (thermostat->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) - dataThermostat[THERMOSTAT_CURRTEMP] = (float)thermostat->curr_roomTemp / 10; - - if (thermostat->daytemp != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_DAYTEMP] = (float)thermostat->daytemp / 2; - if (thermostat->nighttemp != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_NIGHTTEMP] = (float)thermostat->nighttemp / 2; - if (thermostat->holidaytemp != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_HOLIDAYTEMP] = (float)thermostat->holidaytemp / 2; - - if (thermostat->heatingtype != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_HEATINGTYPE] = thermostat->heatingtype; - - if (thermostat->circuitcalctemp != EMS_VALUE_INT_NOTSET) - dataThermostat[THERMOSTAT_CIRCUITCALCTEMP] = thermostat->circuitcalctemp; - } - - // Thermostat Mode - _EMS_THERMOSTAT_MODE thermoMode = _getThermostatMode(hc_v); - if (thermoMode == EMS_THERMOSTAT_MODE_OFF) { - dataThermostat[THERMOSTAT_MODE] = "off"; - } else if (thermoMode == EMS_THERMOSTAT_MODE_MANUAL) { - dataThermostat[THERMOSTAT_MODE] = "manual"; - } else if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { - dataThermostat[THERMOSTAT_MODE] = "auto"; - } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { - dataThermostat[THERMOSTAT_MODE] = "day"; - } else if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { - dataThermostat[THERMOSTAT_MODE] = "night"; - } - } - } - - data[0] = '\0'; // reset data for next package - serializeJson(doc, data, sizeof(data)); - myDebugLog("Publishing thermostat data via MQTT"); - myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); + thermo = publishEMSValues_thermostat(); ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_THERMOSTAT); // unset flag } - // handle the thermostat values - if (ems_getMixingDeviceEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_MIXING) || force)) { - doc.clear(); - JsonObject rootMixing = doc.to(); + if (ems_getBoilerEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_BOILER) || force)) { + boiler = publishEMSValues_boiler(); + ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_BOILER); // unset flag + } - for (uint8_t hc_v = 1; hc_v <= EMS_THERMOSTAT_MAXHC; hc_v++) { - _EMS_Mixing_HC * mixing = &EMS_Mixing.hc[hc_v - 1]; + if (ems_getBoilerEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_SETTINGS))) { + // never force publication of settings + settings = publishEMSValues_settings(); + ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_SETTINGS); // unset flag + } - // only send if we have an active Heating Circuit with real data - if (mixing->active) { - // build new json object - char hc[10]; // hc{1-4} - strlcpy(hc, THERMOSTAT_HC, sizeof(hc)); - strlcat(hc, _int_to_char(s, mixing->hc), sizeof(hc)); - JsonObject dataMixing = rootMixing.createNestedObject(hc); - - if (mixing->flowTemp != EMS_VALUE_SHORT_NOTSET) - dataMixing["flowTemp"] = (float)mixing->flowTemp / 10; - if (mixing->pumpMod != EMS_VALUE_INT_NOTSET) - dataMixing["pumpMod"] = mixing->pumpMod; - if (mixing->valveStatus != EMS_VALUE_INT_NOTSET) - dataMixing["valveStatus"] = mixing->valveStatus; - } - } - - data[0] = '\0'; // reset data for next package - serializeJson(doc, data, sizeof(data)); - myDebugLog("Publishing mixing device data via MQTT"); - myESP.mqttPublish(TOPIC_MIXING_DATA, data); + if (ems_getMixingModuleEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_MIXING) || force)) { + mixing = publishEMSValues_mixing(); ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_MIXING); // unset flag } - // For SM10 and SM100 Solar Modules if (ems_getSolarModuleEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_SOLAR) || force)) { - // build new json object - doc.clear(); - JsonObject rootSM = doc.to(); - - if (EMS_SolarModule.collectorTemp != EMS_VALUE_SHORT_NOTSET) - rootSM[SM_COLLECTORTEMP] = (float)EMS_SolarModule.collectorTemp / 10; - - if (EMS_SolarModule.bottomTemp != EMS_VALUE_SHORT_NOTSET) - rootSM[SM_BOTTOMTEMP] = (float)EMS_SolarModule.bottomTemp / 10; - - if (EMS_SolarModule.pumpModulation != EMS_VALUE_INT_NOTSET) - rootSM[SM_PUMPMODULATION] = EMS_SolarModule.pumpModulation; - - if (EMS_SolarModule.pump != EMS_VALUE_BOOL_NOTSET) { - rootSM[SM_PUMP] = _bool_to_char(s, EMS_SolarModule.pump); - } - - if (EMS_SolarModule.pumpWorkMin != EMS_VALUE_LONG_NOTSET) { - rootSM[SM_PUMPWORKMIN] = (float)EMS_SolarModule.pumpWorkMin; - } - - if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_USHORT_NOTSET) - rootSM[SM_ENERGYLASTHOUR] = (float)EMS_SolarModule.EnergyLastHour / 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] = (float)EMS_SolarModule.EnergyTotal / 10; - - data[0] = '\0'; // reset data for next package - serializeJson(doc, data, sizeof(data)); - myDebugLog("Publishing SM data via MQTT"); - myESP.mqttPublish(TOPIC_SM_DATA, data); + solar = publishEMSValues_solar(); ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_SOLAR); // unset flag } - // handle HeatPump if (ems_getHeatPumpEnabled() && (ems_Device_has_flags(EMS_DEVICE_UPDATE_FLAG_HEATPUMP) || force)) { - // build new json object - doc.clear(); - JsonObject rootSM = doc.to(); - - if (EMS_HeatPump.HPModulation != EMS_VALUE_INT_NOTSET) - rootSM[HP_PUMPMODULATION] = EMS_HeatPump.HPModulation; - - 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)); - myDebugLog("Publishing HeatPump data via MQTT"); - myESP.mqttPublish(TOPIC_HP_DATA, data); + heatpump = publishEMSValues_heatpump(); ems_Device_remove_flags(EMS_DEVICE_UPDATE_FLAG_HEATPUMP); // unset flag } + + if (!thermo && !boiler && !mixing && !solar && !heatpump) { + return false; // nothing was sent + } + + // print + char log_s[60]; + strlcpy(log_s, "Publishing MQTT data for:", sizeof(log_s)); + if (thermo) { + strlcat(log_s, " thermostat", sizeof(log_s)); + } + if (boiler) { + strlcat(log_s, " boiler", sizeof(log_s)); + } + if (settings) { + strlcat(log_s, " settings", sizeof(log_s)); + } + if (mixing) { + strlcat(log_s, " mixing", sizeof(log_s)); + } + if (solar) { + strlcat(log_s, " solar", sizeof(log_s)); + } + if (heatpump) { + strlcat(log_s, " heatpump", sizeof(log_s)); + } + myDebugLog(log_s); + + return true; } -// call PublishValues without forcing +// publishes value via MQTT +void publishValues(bool force, bool send_sensor) { + if (EMSESP_Settings.publish_time == -1) { + return; + } + + // myDebugLog("Starting scheduled MQTT publish..."); + publishEMSValues(force); + if (send_sensor) { + publishSensorValues(); + } + // myESP.heartbeatCheck(true); +} + +// calls publishValues fron the Ticker loop, also sending sensor data +// but not using false for force so only data that has changed will be sent void do_publishValues() { - publishEMSValues(true); // force publish + publishValues(false, true); } // callback to light up the LED, called via Ticker every second @@ -851,6 +1285,7 @@ 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 +// which will cause the values to get published void do_regularUpdates() { if (ems_getBusConnected() && !ems_getTxDisabled()) { myDebugLog("Fetching data from EMS devices"); @@ -860,10 +1295,20 @@ void do_regularUpdates() { } } +// force low-frequency (daily) calls to get data from EMS for the types that aren't sent as broadcasts +// only if we have a EMS connection +// which will cause the values to get published +void do_dailyUpdates() { + if (ems_getBusConnected() && !ems_getTxDisabled()) { + myDebugLog("Fetching settings data"); + ems_getSettingsValues(); + } +} + // turn back on the hot water for the shower void _showerColdShotStop() { if (EMSESP_Shower.doingColdShot) { - myDebugLog("[Shower] finished shot of cold. hot water back on"); + myDebugLog("[Shower] finished shot of cold water. Hot water turned back on"); ems_setWarmTapWaterActivated(true); EMSESP_Shower.doingColdShot = false; showerColdShotStopTimer.detach(); // disable the timer @@ -873,7 +1318,7 @@ void _showerColdShotStop() { // turn off hot water to send a shot of cold void _showerColdShotStart() { if (EMSESP_Settings.shower_alert) { - myDebugLog("[Shower] doing a shot of cold water"); + myDebugLog("[Shower] starting shot of cold water"); ems_setWarmTapWaterActivated(false); EMSESP_Shower.doingColdShot = true; // start the timer for n seconds which will reset the water back to hot @@ -887,6 +1332,7 @@ void runUnitTest(uint8_t test_num) { publishValuesTimer.detach(); systemCheckTimer.detach(); regularUpdatesTimer.detach(); + dailyUpdatesTimer.detach(); // EMSESP_Settings.listen_mode = true; // temporary go into listen mode to disable Tx ems_testTelegram(test_num); } @@ -900,8 +1346,6 @@ bool LoadSaveCallback(MYESP_FSACTION_t action, JsonObject settings) { return false; } - // serializeJsonPretty(settings, Serial); // for debugging - EMSESP_Settings.led = settings["led"]; EMSESP_Settings.led_gpio = settings["led_gpio"] | EMSESP_LED_GPIO; EMSESP_Settings.dallas_gpio = settings["dallas_gpio"] | EMSESP_DALLAS_GPIO; @@ -916,19 +1360,30 @@ bool LoadSaveCallback(MYESP_FSACTION_t action, JsonObject settings) { EMSESP_Settings.tx_mode = settings["tx_mode"] | EMS_TXMODE_DEFAULT; // default to 1 (generic) ems_setTxMode(EMSESP_Settings.tx_mode); + EMSESP_Settings.master_thermostat = settings["master_thermostat"] | 0; // default to 0 (none) + ems_setMasterThermostat(EMSESP_Settings.master_thermostat); + + EMSESP_Settings.bus_id = settings["bus_id"] | EMS_BUSID_DEFAULT; // default to 0x0B (Service Key) + ems_setEMSbusid(EMSESP_Settings.bus_id); + + EMSESP_Settings.known_devices = strdup(settings["known_devices"] | ""); + return true; } if (action == MYESP_FSACTION_SAVE) { - settings["led"] = EMSESP_Settings.led; - settings["led_gpio"] = EMSESP_Settings.led_gpio; - settings["dallas_gpio"] = EMSESP_Settings.dallas_gpio; - settings["dallas_parasite"] = EMSESP_Settings.dallas_parasite; - settings["listen_mode"] = EMSESP_Settings.listen_mode; - settings["shower_timer"] = EMSESP_Settings.shower_timer; - settings["shower_alert"] = EMSESP_Settings.shower_alert; - settings["publish_time"] = EMSESP_Settings.publish_time; - settings["tx_mode"] = EMSESP_Settings.tx_mode; + settings["led"] = EMSESP_Settings.led; + settings["led_gpio"] = EMSESP_Settings.led_gpio; + settings["dallas_gpio"] = EMSESP_Settings.dallas_gpio; + settings["dallas_parasite"] = EMSESP_Settings.dallas_parasite; + settings["listen_mode"] = EMSESP_Settings.listen_mode; + settings["shower_timer"] = EMSESP_Settings.shower_timer; + settings["shower_alert"] = EMSESP_Settings.shower_alert; + settings["publish_time"] = EMSESP_Settings.publish_time; + settings["tx_mode"] = EMSESP_Settings.tx_mode; + settings["bus_id"] = EMSESP_Settings.bus_id; + settings["master_thermostat"] = EMSESP_Settings.master_thermostat; + settings["known_devices"] = EMSESP_Settings.known_devices; return true; } @@ -936,48 +1391,21 @@ bool LoadSaveCallback(MYESP_FSACTION_t action, JsonObject settings) { return false; } -// Publish shower data -bool do_publishShowerData() { - StaticJsonDocument<200> doc; - JsonObject rootShower = doc.to(); - rootShower[TOPIC_SHOWER_TIMER] = EMSESP_Settings.shower_timer ? "1" : "0"; - rootShower[TOPIC_SHOWER_ALERT] = EMSESP_Settings.shower_alert ? "1" : "0"; - - // only publish shower duration if there is a value - char s[50] = {0}; - if (EMSESP_Shower.duration > SHOWER_MIN_DURATION) { - char buffer[16] = {0}; - strlcpy(s, itoa((uint8_t)((EMSESP_Shower.duration / (1000 * 60)) % 60), buffer, 10), sizeof(s)); - strlcat(s, " minutes and ", sizeof(s)); - strlcat(s, itoa((uint8_t)((EMSESP_Shower.duration / 1000) % 60), buffer, 10), sizeof(s)); - strlcat(s, " seconds", sizeof(s)); - rootShower[TOPIC_SHOWER_DURATION] = s; - } - - char data[300] = {0}; - serializeJson(doc, data, sizeof(data)); - - myDebugLog("Publishing shower data via MQTT"); - - // Publish MQTT forcing retain to be off - return (myESP.mqttPublish(TOPIC_SHOWER_DATA, data, false)); -} - // callback for custom settings when showing Stored Settings with the 'set' command // wc is number of arguments after the 'set' command // returns true if the setting was recognized and changed and should be saved back to SPIFFs -bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, const char * value) { - bool ok = false; +MYESP_FSACTION_t SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, const char * value) { + MYESP_FSACTION_t ok = MYESP_FSACTION_ERR; if (action == MYESP_FSACTION_SET) { // led if ((strcmp(setting, "led") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { EMSESP_Settings.led = true; - ok = true; + ok = MYESP_FSACTION_RESTART; } else if (strcmp(value, "off") == 0) { EMSESP_Settings.led = false; - ok = true; + ok = MYESP_FSACTION_RESTART; // let's make sure LED is really off - For onboard high=off digitalWrite(EMSESP_Settings.led_gpio, (EMSESP_Settings.led_gpio == LED_BUILTIN) ? HIGH : LOW); } else { @@ -989,12 +1417,12 @@ bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, if ((strcmp(setting, "listen_mode") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { EMSESP_Settings.listen_mode = true; - ok = true; + ok = MYESP_FSACTION_OK; myDebug_P(PSTR("* in listen mode. All Tx is disabled.")); ems_setTxDisabled(true); } else if (strcmp(value, "off") == 0) { EMSESP_Settings.listen_mode = false; - ok = true; + ok = MYESP_FSACTION_OK; ems_setTxDisabled(false); myDebug_P(PSTR("* out of listen mode. Tx is now enabled.")); } else { @@ -1008,23 +1436,23 @@ bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, // reset pin pinMode(EMSESP_Settings.led_gpio, OUTPUT); digitalWrite(EMSESP_Settings.led_gpio, (EMSESP_Settings.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off - ok = true; + ok = MYESP_FSACTION_OK; } // dallas_gpio if ((strcmp(setting, "dallas_gpio") == 0) && (wc == 2)) { EMSESP_Settings.dallas_gpio = atoi(value); - ok = true; + ok = MYESP_FSACTION_RESTART; } // dallas_parasite if ((strcmp(setting, "dallas_parasite") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { EMSESP_Settings.dallas_parasite = true; - ok = true; + ok = MYESP_FSACTION_RESTART; } else if (strcmp(value, "off") == 0) { EMSESP_Settings.dallas_parasite = false; - ok = true; + ok = MYESP_FSACTION_RESTART; } else { myDebug_P(PSTR("Error. Usage: set dallas_parasite ")); } @@ -1033,11 +1461,13 @@ bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, // shower timer if ((strcmp(setting, "shower_timer") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { + do_publishShowerData(); EMSESP_Settings.shower_timer = true; - ok = do_publishShowerData(); + ok = MYESP_FSACTION_OK; } else if (strcmp(value, "off") == 0) { + do_publishShowerData(); EMSESP_Settings.shower_timer = false; - ok = do_publishShowerData(); + ok = MYESP_FSACTION_OK; } else { myDebug_P(PSTR("Error. Usage: set shower_timer ")); } @@ -1046,20 +1476,26 @@ bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, // shower alert if ((strcmp(setting, "shower_alert") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { + do_publishShowerData(); EMSESP_Settings.shower_alert = true; - ok = do_publishShowerData(); + ok = MYESP_FSACTION_OK; } else if (strcmp(value, "off") == 0) { + do_publishShowerData(); EMSESP_Settings.shower_alert = false; - ok = do_publishShowerData(); + ok = MYESP_FSACTION_OK; } else { myDebug_P(PSTR("Error. Usage: set shower_alert ")); } } // publish_time - if ((strcmp(setting, "publish_time") == 0) && (wc == 2)) { - EMSESP_Settings.publish_time = atoi(value); - ok = true; + if (strcmp(setting, "publish_time") == 0) { + if (wc == 1) { + EMSESP_Settings.publish_time = 0; + } else { + EMSESP_Settings.publish_time = atoi(value); + } + ok = MYESP_FSACTION_RESTART; } // tx_mode @@ -1068,11 +1504,51 @@ bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, if ((mode >= 1) && (mode <= 3)) { // see ems.h for definitions EMSESP_Settings.tx_mode = mode; ems_setTxMode(mode); - ok = true; + ok = MYESP_FSACTION_RESTART; } else { myDebug_P(PSTR("Error. Usage: set tx_mode <1 | 2 | 3>")); } } + + // bus_id + if ((strcmp(setting, "bus_id") == 0) && (wc == 2)) { + uint8_t id = strtoul(value, 0, 16); + if ((id == 0x0B) || (id == 0x0D) || (id == 0x0A) || (id == 0x0F) || (id == 0x12)) { + EMSESP_Settings.bus_id = id; + ems_setEMSbusid(id); + ok = MYESP_FSACTION_RESTART; + } else { + myDebug_P(PSTR("Error. Usage: set bus_id , with ID=0B, 0D, 0A, 0F or 12")); + } + } + + // master_thermostat + if (strcmp(setting, "master_thermostat") == 0) { + if (wc == 1) { + // show list + char device_string[100]; + myDebug_P(PSTR("Available thermostat Product ids:")); + for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + if (it->device_type == EMS_DEVICE_TYPE_THERMOSTAT) { + strlcpy(device_string, (it)->device_desc_p, sizeof(device_string)); + myDebug_P(PSTR(" %d = %s"), (it)->product_id, device_string); + } + } + myDebug_P(""); + myDebug_P(PSTR("Usage: set master_thermostat ")); + ems_setMasterThermostat(0); // default value + EMSESP_Settings.master_thermostat = 0; // back to default + ok = MYESP_FSACTION_OK; + } else if (wc == 2) { + uint8_t pid = atoi(value); + EMSESP_Settings.master_thermostat = pid; + ems_setMasterThermostat(pid); + // force a scan again to detect and set the thermostat + Devices.clear(); + ems_doReadCommand(EMS_TYPE_UBADevices, EMS_Boiler.device_id); + ok = MYESP_FSACTION_OK; + } + } } if (action == MYESP_FSACTION_LIST) { @@ -1081,13 +1557,23 @@ bool SetListCallback(MYESP_FSACTION_t action, uint8_t wc, const char * setting, myDebug_P(PSTR(" dallas_gpio=%d"), EMSESP_Settings.dallas_gpio); myDebug_P(PSTR(" dallas_parasite=%s"), EMSESP_Settings.dallas_parasite ? "on" : "off"); myDebug_P(PSTR(" tx_mode=%d"), EMSESP_Settings.tx_mode); + myDebug_P(PSTR(" bus_id=0x%02X"), EMSESP_Settings.bus_id); myDebug_P(PSTR(" listen_mode=%s"), EMSESP_Settings.listen_mode ? "on" : "off"); myDebug_P(PSTR(" shower_timer=%s"), EMSESP_Settings.shower_timer ? "on" : "off"); myDebug_P(PSTR(" shower_alert=%s"), EMSESP_Settings.shower_alert ? "on" : "off"); - if (EMSESP_Settings.publish_time) { - myDebug_P(PSTR(" publish_time=%d"), EMSESP_Settings.publish_time); + + if (EMSESP_Settings.publish_time == 0) { + myDebug_P(PSTR(" publish_time=0 (automatic)")); + } else if (EMSESP_Settings.publish_time == -1) { + myDebug_P(PSTR(" publish_time=-1 (disabled)")); } else { - myDebug_P(PSTR(" publish_time=0 (always publish when data received)"), EMSESP_Settings.publish_time); + myDebug_P(PSTR(" publish_time=%d"), EMSESP_Settings.publish_time); + } + + if (EMSESP_Settings.master_thermostat) { + myDebug_P(PSTR(" master_thermostat=%d"), EMSESP_Settings.master_thermostat); + } else { + myDebug_P(PSTR(" master_thermostat=0 (use first detected)")); } } @@ -1132,14 +1618,40 @@ void _showCommands(uint8_t event) { // we set the logging here void TelnetCallback(uint8_t event) { if (event == TELNET_EVENT_CONNECT) { - ems_setLogging(EMS_SYS_LOGGING_DEFAULT); + ems_setLogging(EMS_SYS_LOGGING_DEFAULT, true); } else if (event == TELNET_EVENT_DISCONNECT) { - ems_setLogging(EMS_SYS_LOGGING_NONE); + ems_setLogging(EMS_SYS_LOGGING_NONE, true); } else if ((event == TELNET_EVENT_SHOWCMD) || (event == TELNET_EVENT_SHOWSET)) { _showCommands(event); } } +void clearEMSDevices() { + EMSESP_Settings.known_devices = strdup(""); + myESP.saveSettings(); +} + +// get the list of know devices, as a string, and save them to the config file +void saveEMSDevices() { + if (Devices.empty()) { + return; + } + + char s[100]; + char buffer[16] = {0}; + s[0] = '\0'; + + for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + strlcat(s, _hextoa(it->device_id, buffer), sizeof(s)); + strlcat(s, " ", sizeof(s)); + } + + strlcpy(EMSESP_Settings.known_devices, s, sizeof(s)); + + myDebug_P(PSTR("The device IDs %s%s%swill be automatically scanned when EMS-ESP boots up."), COLOR_BOLD_ON, EMSESP_Settings.known_devices, COLOR_BOLD_OFF); + myESP.saveSettings(); +} + // extra commands options for telnet debug window // wc is the word count, i.e. number of arguments. Everything is in lower case. void TelnetCommandCallback(uint8_t wc, const char * commandLine) { @@ -1154,40 +1666,50 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { if (strcmp(first_cmd, "publish") == 0) { do_publishValues(); - publishSensorValues(); - do_publishShowerData(); ok = true; } if (strcmp(first_cmd, "refresh") == 0) { do_regularUpdates(); + do_dailyUpdates(); ok = true; } if (strcmp(first_cmd, "devices") == 0) { - ems_printDevices(); - ok = true; + if (wc == 1) { + ems_printDevices(); + return; + } + + // wc = 2 or more. check for "scan" + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "scan") == 0) { + // just scan use UBA 0x07 telegram + myDebug_P(PSTR("Requesting EMS bus master for its device list and scanning for external sensors...")); + scanDallas(); + Devices.clear(); // init the device map + ems_doReadCommand(EMS_TYPE_UBADevices, EMS_Boiler.device_id); + return; + } else if (strcmp(second_cmd, "scan+") == 0) { + myDebug_P(PSTR("Started deep scan of EMS bus for our known devices. This can take up to 10 seconds...")); + Devices.clear(); // init the device map + ems_scanDevices(); + return; + } else if (strcmp(second_cmd, "save") == 0) { + saveEMSDevices(); + return; + } else if (strcmp(second_cmd, "clear") == 0) { + clearEMSDevices(); + return; + } + ok = false; // unknown command } - if (strcmp(first_cmd, "queue") == 0) { + if (strcmp(first_cmd, "txqueue") == 0) { ems_printTxQueue(); ok = true; } - if (strcmp(first_cmd, "autodetect") == 0) { - if (wc == 2) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "scan") == 0) { - ems_scanDevices(); // known device scan - ok = true; - } - } else { - ems_clearDeviceList(); - ems_doReadCommand(EMS_TYPE_UBADevices, EMS_Boiler.device_id); - ok = true; - } - } - // logging if ((strcmp(first_cmd, "log") == 0) && (wc >= 2)) { char * second_cmd = _readWord(); @@ -1203,6 +1725,9 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } else if (strcmp(second_cmd, "s") == 0) { ems_setLogging(EMS_SYS_LOGGING_SOLARMODULE); ok = true; + } else if (strcmp(second_cmd, "m") == 0) { + ems_setLogging(EMS_SYS_LOGGING_MIXINGMODULE); + ok = true; } else if (strcmp(second_cmd, "r") == 0) { ems_setLogging(EMS_SYS_LOGGING_RAW); ok = true; @@ -1215,25 +1740,35 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } else if ((strcmp(second_cmd, "w") == 0) && (wc == 3)) { ems_setLogging(EMS_SYS_LOGGING_WATCH, _readHexNumber()); // get type_id ok = true; + } else if ((strcmp(second_cmd, "d") == 0) && (wc == 3)) { + ems_setLogging(EMS_SYS_LOGGING_DEVICE, _readHexNumber()); // get device_id + ok = true; } } // thermostat commands + // thermostat temp if ((strcmp(first_cmd, "thermostat") == 0) && (wc >= 3)) { - char * second_cmd = _readWord(); - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC; + char * second_cmd = _readWord(); if (strcmp(second_cmd, "temp") == 0) { - if (wc == 4) { - hc = _readIntNumber(); // next parameter is the heating circuit + float temp = _readFloatNumber(); // read in next param which is the temp + + if (wc == 3) { + // no more params + ems_setThermostatTemp(temp, EMS_THERMOSTAT_DEFAULTHC, EMS_THERMOSTAT_MODE_AUTO); + ok = true; + } else { + // get modevalue and heatingcircuit + char * mode_s = _readWord(); // get mode string next + uint8_t hc = (wc == 4) ? EMS_THERMOSTAT_DEFAULTHC : _readIntNumber(); + ems_setThermostatTemp(temp, hc, mode_s); + ok = true; } - ems_setThermostatTemp(_readFloatNumber(), hc); - ok = true; } else if (strcmp(second_cmd, "mode") == 0) { - if (wc == 4) { - hc = _readIntNumber(); // next parameter is the heating circuit - } - ems_setThermostatMode(_readIntNumber(), hc); + char * mode_s = _readWord(); + uint8_t hc = (wc == 3) ? EMS_THERMOSTAT_DEFAULTHC : _readIntNumber(); + ems_setThermostatMode(mode_s, hc); ok = true; } else if (strcmp(second_cmd, "read") == 0) { ems_doReadCommand(_readHexNumber(), EMS_Thermostat.device_id); @@ -1274,6 +1809,24 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { } else if (strcmp(second_cmd, "flowtemp") == 0) { ems_setFlowTemp(_readIntNumber()); ok = true; + } else if (strcmp(second_cmd, "wwactive") == 0) { + char * third_cmd = _readWord(); + if (strcmp(third_cmd, "on") == 0) { + ems_setWarmWaterActivated(true); + ok = true; + } else if (strcmp(third_cmd, "off") == 0) { + ems_setWarmWaterActivated(false); + ok = true; + } + } else if (strcmp(second_cmd, "wwonetime") == 0) { + char * third_cmd = _readWord(); + if (strcmp(third_cmd, "on") == 0) { + ems_setWarmWaterOnetime(true); + ok = true; + } else if (strcmp(third_cmd, "off") == 0) { + ems_setWarmWaterOnetime(false); + ok = true; + } } } @@ -1350,6 +1903,9 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { strlcat(topic_s, itoa(hc, buffer, 10), sizeof(topic_s)); myESP.mqttSubscribe(topic_s); } + // also subscribe without the HC appended to the end of the topic + myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_TEMP_HA); + myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE_HA); // generic incoming MQTT command for Thermostat // this is used for example for setting daytemp, nighttemp, holidaytemp @@ -1359,11 +1915,15 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { // this is used for example for comfort, flowtemp myESP.mqttSubscribe(TOPIC_BOILER_CMD); - // these three need to be unqiue topics + // these need to be unique topics myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWACTIVATED); myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWONETIME); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWCIRCULATION); myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); + // generic incoming MQTT command for Settings + myESP.mqttSubscribe(TOPIC_SETTINGS_CMD); + // generic incoming MQTT command for EMS-ESP // this is used for example for shower_coldshot myESP.mqttSubscribe(TOPIC_GENERIC_CMD); @@ -1395,6 +1955,8 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { const char * command = doc["cmd"]; // Check whatever the command is and act accordingly + + // we only have one now, this is for the shower coldshot if (strcmp(command, TOPIC_SHOWER_COLDSHOT) == 0) { _showerColdShotStart(); return; @@ -1416,14 +1978,18 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { const char * shower_alert = doc[TOPIC_SHOWER_ALERT]; if (shower_alert) { EMSESP_Settings.shower_alert = ((shower_alert[0] - MYESP_MQTT_PAYLOAD_OFF) == 1); - myDebug_P(PSTR("Shower alert has been set to %s"), EMSESP_Settings.shower_alert ? "enabled" : "disabled"); + if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + myDebug_P(PSTR("Shower alert has been set to %s"), EMSESP_Settings.shower_alert ? "enabled" : "disabled"); + } } // assumes payload is "1" or "0" const char * shower_timer = doc[TOPIC_SHOWER_TIMER]; if (shower_timer) { EMSESP_Settings.shower_timer = ((shower_timer[0] - MYESP_MQTT_PAYLOAD_OFF) == 1); - myDebug_P(PSTR("Shower timer has been set to %s"), EMSESP_Settings.shower_timer ? "enabled" : "disabled"); + if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + myDebug_P(PSTR("Shower timer has been set to %s"), EMSESP_Settings.shower_timer ? "enabled" : "disabled"); + } } return; @@ -1439,10 +2005,16 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { return; } const char * command = doc["cmd"]; + if (command == nullptr) { + return; + } // boiler ww comfort setting if (strcmp(command, TOPIC_BOILER_CMD_COMFORT) == 0) { const char * data = doc["data"]; + if (data == nullptr) { + return; + } if (strcmp((char *)data, "hot") == 0) { ems_setWarmWaterModeComfort(1); } else if (strcmp((char *)data, "comfort") == 0) { @@ -1456,7 +2028,9 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { // boiler flowtemp setting if (strcmp(command, TOPIC_BOILER_CMD_FLOWTEMP) == 0) { uint8_t t = doc["data"]; - ems_setFlowTemp(t); + if (t) { + ems_setFlowTemp(t); + } return; } @@ -1477,43 +2051,115 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { // wwOneTime if (strcmp(topic, TOPIC_BOILER_CMD_WWONETIME) == 0) { - if (message[0] == '1' || strcmp(message, "on") == 0) { + if (message[0] == MYESP_MQTT_PAYLOAD_ON || strcmp(message, "on") == 0) { ems_setWarmWaterOnetime(true); - } else if (message[0] == '0' || strcmp(message, "off") == 0) { + } else if (message[0] == MYESP_MQTT_PAYLOAD_OFF || strcmp(message, "off") == 0) { ems_setWarmWaterOnetime(false); } return; } + // wwCirculation + if (strcmp(topic, TOPIC_BOILER_CMD_WWCIRCULATION) == 0) { + if (message[0] == MYESP_MQTT_PAYLOAD_ON || strcmp(message, "on") == 0) { + ems_setWarmWaterCirculation(true); + } else if (message[0] == MYESP_MQTT_PAYLOAD_OFF || strcmp(message, "off") == 0) { + ems_setWarmWaterCirculation(false); + } + return; + } + // boiler wwtemp changes if (strcmp(topic, TOPIC_BOILER_CMD_WWTEMP) == 0) { uint8_t t = atoi((char *)message); - ems_setWarmWaterTemp(t); - publishEMSValues(true); + if (t) { + ems_setWarmWaterTemp(t); + publishEMSValues(true); + } return; } + // check for settings commands + if (strcmp(topic, TOPIC_SETTINGS_CMD) == 0) { + // convert JSON and get the command + StaticJsonDocument<100> doc; + DeserializationError error = deserializeJson(doc, message); // Deserialize the JSON document + if (error) { + myDebug_P(PSTR("[MQTT] Invalid command from topic %s, payload %s, error %s"), topic, message, error.c_str()); + return; + } + const char * command = doc["cmd"]; + if (command == nullptr) { + return; + } + + // language setting + if (strcmp(command, TOPIC_SETTINGS_CMD_LANGUAGE) == 0) { + const char * data = doc["data"]; + if (data == nullptr) { + return; + } + if (strcasecmp((char *)data, "french") == 0) { + ems_setSettingsLanguage(EMS_VALUE_IBASettings_LANG_FRENCH); + } else if (strcasecmp((char *)data, "german") == 0) { + ems_setSettingsLanguage(EMS_VALUE_IBASettings_LANG_GERMAN); + } else if (strcasecmp((char *)data, "italian") == 0) { + ems_setSettingsLanguage(EMS_VALUE_IBASettings_LANG_ITALIAN); + } + return; + } + // building setting + if (strcmp(command, TOPIC_SETTINGS_CMD_BUILDING) == 0) { + const char * data = doc["data"]; + if (data == nullptr) { + return; + } + if (strcasecmp((char *)data, "light") == 0) { + ems_setSettingsBuilding(EMS_VALUE_IBASettings_BUILDING_LIGHT); + } else if (strcasecmp((char *)data, "medium") == 0) { + ems_setSettingsBuilding(EMS_VALUE_IBASettings_BUILDING_MEDIUM); + } else if (strcasecmp((char *)data, "heavy") == 0) { + ems_setSettingsBuilding(EMS_VALUE_IBASettings_BUILDING_HEAVY); + } + return; + } + // display setting + if (strcmp(command, TOPIC_SETTINGS_CMD_DISPLAY) == 0) { + const char * data = doc["data"]; + if (data == nullptr) { + return; + } + uint8_t t = atoi((char *)data); + if (t) { + ems_setSettingsDisplay(t - 1); + } + return; + } + return; // unknown settings command + } + + uint8_t hc; // thermostat temp changes hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_TEMP_HA, topic); if (hc) { - float f = strtof((char *)message, 0); - ems_setThermostatTemp(f, hc); - publishEMSValues(true); // publish back immediately - return; + if (EMS_Thermostat.hc[hc - 1].active) { + float f = strtof((char *)message, 0); + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_AUTO); + publishEMSValues(true); // publish back immediately + return; + } + } } // thermostat mode changes hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_MODE_HA, topic); if (hc) { - if (strncmp(message, "auto", 4) == 0) { - ems_setThermostatMode(2, hc); - } else if ((strncmp(message, "day", 4) == 0) || (strncmp(message, "manual", 6) == 0) || (strncmp(message, "heat", 4) == 0)) { - ems_setThermostatMode(1, hc); - } else if ((strncmp(message, "night", 5) == 0) || (strncmp(message, "off", 3) == 0)) { - ems_setThermostatMode(0, hc); + if (EMS_Thermostat.hc[hc - 1].active) { + ems_setThermostatMode(message, hc); + return; } - return; } // check for generic thermostat commands @@ -1526,52 +2172,106 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { return; } const char * command = doc["cmd"]; + if (command == nullptr) { + return; + } // thermostat temp changes hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_TEMP, command); if (hc) { - float f = doc["data"]; - ems_setThermostatTemp(f, hc); - publishEMSValues(true); // publish back immediately - return; + if (EMS_Thermostat.hc[hc - 1].active) { + float f = doc["data"]; + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_AUTO); + publishEMSValues(true); // publish back immediately + return; + } + } } // thermostat mode changes hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_MODE, command); if (hc) { - const char * data_cmd = doc["data"]; - if (strncmp(data_cmd, "auto", 4) == 0) { - ems_setThermostatMode(2, hc); - } else if ((strncmp(data_cmd, "day", 4) == 0) || (strncmp(data_cmd, "manual", 6) == 0) || (strncmp(data_cmd, "heat", 4) == 0)) { - ems_setThermostatMode(1, hc); - } else if ((strncmp(data_cmd, "night", 5) == 0) || (strncmp(data_cmd, "off", 3) == 0)) { - ems_setThermostatMode(0, hc); + if (EMS_Thermostat.hc[hc - 1].active) { + const char * data_cmd = doc["data"]; + if (data_cmd == nullptr) { + return; + } + ems_setThermostatMode(data_cmd, hc); + return; } - return; } // set night temp value hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_NIGHTTEMP, command); if (hc) { - float f = doc["data"]; - ems_setThermostatTemp(f, hc, 1); // night - return; + if (EMS_Thermostat.hc[hc - 1].active) { + float f = doc["data"]; + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_NIGHT); // night + return; + } + } } // set daytemp value hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_DAYTEMP, command); if (hc) { - float f = doc["data"]; - ems_setThermostatTemp(f, hc, 2); // day - return; + if (EMS_Thermostat.hc[hc - 1].active) { + float f = doc["data"]; + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_DAY); // day + } + return; + } } // set holiday value hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP, command); if (hc) { - float f = doc["data"]; - ems_setThermostatTemp(f, hc, 3); // holiday - return; + if (EMS_Thermostat.hc[hc - 1].active) { + float f = doc["data"]; + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_HOLIDAY); // holiday + } + return; + } + } + + // set eco value + hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_ECOTEMP, command); + if (hc) { + if (EMS_Thermostat.hc[hc - 1].active) { + float f = doc["data"]; + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_ECO); // holiday + } + return; + } + } + + // set heat value + hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_HEATTEMP, command); + if (hc) { + if (EMS_Thermostat.hc[hc - 1].active) { + float f = doc["data"]; + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_HEAT); // holiday + } + return; + } + } + + // set nofrost value + hc = _hasHCspecified(TOPIC_THERMOSTAT_CMD_NOFROSTTEMP, command); + if (hc) { + if (EMS_Thermostat.hc[hc - 1].active) { + float f = doc["data"]; + if (f) { + ems_setThermostatTemp(f, hc, EMS_THERMOSTAT_MODE_NOFROST); // holiday + } + return; + } } } } @@ -1647,16 +2347,17 @@ void WebCallback(JsonObject root) { char buffer[200]; thermostat["tm"] = ems_getDeviceDescription(EMS_DEVICE_TYPE_THERMOSTAT, buffer, true); - uint8_t hc_num = EMS_THERMOSTAT_DEFAULTHC; // default to HC1 - uint8_t model = ems_getThermostatModel(); - + uint8_t hc_num = 1; // default to HC1 + uint8_t model = ems_getThermostatFlags(); + while (hc_num < EMS_THERMOSTAT_MAXHC && !EMS_Thermostat.hc[hc_num - 1].active) + hc_num++; // first active hc // Render Current & Setpoint Room Temperature if (model == EMS_DEVICE_FLAG_EASY) { if (EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) thermostat["ts"] = (float)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 100; if (EMS_Thermostat.hc[hc_num - 1].curr_roomTemp != EMS_VALUE_SHORT_NOTSET) thermostat["tc"] = (float)EMS_Thermostat.hc[hc_num - 1].curr_roomTemp / 100; - } else if (model == EMS_DEVICE_FLAG_JUNKERS) { + } else if ((model == EMS_DEVICE_FLAG_JUNKERS1) || (model == EMS_DEVICE_FLAG_JUNKERS2)) { if (EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) thermostat["ts"] = (float)EMS_Thermostat.hc[hc_num - 1].setpoint_roomTemp / 10; if (EMS_Thermostat.hc[hc_num - 1].curr_roomTemp != EMS_VALUE_SHORT_NOTSET) @@ -1676,6 +2377,10 @@ void WebCallback(JsonObject root) { thermostat["tmode"] = "manual"; } else if (thermoMode == EMS_THERMOSTAT_MODE_AUTO) { thermostat["tmode"] = "auto"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_DAY) { + thermostat["tmode"] = "day"; + } else if (thermoMode == EMS_THERMOSTAT_MODE_NIGHT) { + thermostat["tmode"] = "night"; } } else { thermostat["ok"] = false; @@ -1691,23 +2396,23 @@ void WebCallback(JsonObject root) { boiler["b1"] = (EMS_Boiler.tapwaterActive ? "running" : "off"); boiler["b2"] = (EMS_Boiler.heatingActive ? "active" : "off"); - if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET) + if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET) { boiler["b3"] = EMS_Boiler.selFlowTemp; + } - if (EMS_Boiler.curFlowTemp != EMS_VALUE_INT_NOTSET) + if (EMS_Boiler.curFlowTemp != EMS_VALUE_INT_NOTSET) { boiler["b4"] = EMS_Boiler.curFlowTemp / 10; + } - if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET) - boiler["b5"] = (float)EMS_Boiler.boilTemp / 10; - - if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET) - boiler["b6"] = (float)EMS_Boiler.retTemp / 10; + if (EMS_Boiler.wWCurTmp != EMS_VALUE_USHORT_NOTSET) { + boiler["b5"] = (float)EMS_Boiler.wWCurTmp / 10; + } } else { boiler["ok"] = false; } - // For SM10/SM100 Solar Module + // For SM10/SM100/SM200 Solar Module JsonObject sm = root.createNestedObject("sm"); if (ems_getSolarModuleEnabled()) { sm["ok"] = true; @@ -1729,13 +2434,13 @@ void WebCallback(JsonObject root) { sm["sm4"] = _bool_to_char(s, EMS_SolarModule.pump); // Pump active on/off } - if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_USHORT_NOTSET) + if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_LONG_NOTSET) sm["sm5"] = (float)EMS_SolarModule.EnergyLastHour / 10; // Energy last hour Wh - if (EMS_SolarModule.EnergyToday != EMS_VALUE_USHORT_NOTSET) // Energy today Wh + if (EMS_SolarModule.EnergyToday != EMS_VALUE_LONG_NOTSET) // Energy today Wh sm["sm6"] = EMS_SolarModule.EnergyToday; - if (EMS_SolarModule.EnergyTotal != EMS_VALUE_USHORT_NOTSET) // Energy total KWh + if (EMS_SolarModule.EnergyTotal != EMS_VALUE_LONG_NOTSET) // Energy total KWh sm["sm7"] = (float)EMS_SolarModule.EnergyTotal / 10; } else { sm["ok"] = false; @@ -1765,15 +2470,18 @@ void WebCallback(JsonObject root) { // Most of these will be overwritten after the SPIFFS config file is loaded void initEMSESP() { // general settings - EMSESP_Settings.shower_timer = false; - EMSESP_Settings.shower_alert = false; - EMSESP_Settings.led = true; // LED is on by default - EMSESP_Settings.listen_mode = false; - EMSESP_Settings.publish_time = DEFAULT_PUBLISHTIME; - EMSESP_Settings.dallas_sensors = 0; - EMSESP_Settings.led_gpio = EMSESP_LED_GPIO; - EMSESP_Settings.dallas_gpio = EMSESP_DALLAS_GPIO; - EMSESP_Settings.tx_mode = EMS_TXMODE_DEFAULT; // default tx mode + EMSESP_Settings.shower_timer = false; + EMSESP_Settings.shower_alert = false; + EMSESP_Settings.led = true; // LED is on by default + EMSESP_Settings.listen_mode = false; + EMSESP_Settings.publish_time = DEFAULT_PUBLISHTIME; + EMSESP_Settings.dallas_sensors = 0; + EMSESP_Settings.led_gpio = EMSESP_LED_GPIO; + EMSESP_Settings.dallas_gpio = EMSESP_DALLAS_GPIO; + EMSESP_Settings.tx_mode = EMS_TXMODE_DEFAULT; // default tx mode + EMSESP_Settings.bus_id = EMS_BUSID_DEFAULT; // Service Key is default + EMSESP_Settings.master_thermostat = 0; + EMSESP_Settings.known_devices = strdup(""); // shower settings EMSESP_Shower.timerStart = 0; @@ -1789,6 +2497,10 @@ void initEMSESP() { * Shower Logic */ void showerCheck() { + if (!EMSESP_Settings.shower_timer) { + return; + } + uint32_t time_now = millis(); // if already in cold mode, ignore all this logic until we're out of the cold blast if (!EMSESP_Shower.doingColdShot) { @@ -1845,13 +2557,45 @@ void showerCheck() { } } +// this is called when we've first established an EMS connection +// go send out some spies to figure out what is on the bus and who's driving it +void startupEMSscan() { + if (EMSESP_Settings.listen_mode) { + return; + } + + // First scan anything we may have saved as known devices from a "devices save" command + if (strlen(EMSESP_Settings.known_devices) > 0) { + char * p; + char * temp = strdup(EMSESP_Settings.known_devices); // because strlok is destructive, make a copy + char value[10] = {0}; + // get first value + if ((p = strtok(temp, " "))) { // delimiter + strlcpy(value, p, sizeof(value)); + uint8_t val = (uint8_t)strtol(value, 0, 16); + ems_doReadCommand(EMS_TYPE_Version, val); + } + // and iterate until end + while (p != 0) { + if ((p = strtok(nullptr, " "))) { + strlcpy(value, p, sizeof(value)); + uint8_t val = (uint8_t)strtol(value, 0, 16); + ems_doReadCommand(EMS_TYPE_Version, val); + } + } + } + + // ask the Boiler to show us it's attached devices in case we missed anything + ems_discoverModels(); +} + // // SETUP // void setup() { - // GPIO15/D8 has a pull down, so we must set it to HIGH so it doesn't bring the whole EMS bus down + // GPIO15/D8 has an onboard pull down resistor and we use this for Tx, so force it high so it doesn't bring the whole EMS bus down pinMode(D8, OUTPUT); - digitalWrite(D8, 1); + digitalWrite(D8, HIGH); // init our own parameters initEMSESP(); @@ -1880,22 +2624,21 @@ void setup() { myESP.setUseSerial(false); emsuart_init(); // start EMS bus transmissions myDebug_P(PSTR("[UART] Rx/Tx connection established")); - if (!EMSESP_Settings.listen_mode) { - // go and find the boiler and thermostat types, if not in listen mode - ems_discoverModels(); - } + startupEMSscan(); } - // enable regular checks + // enable regular checks to fetch data and publish using Tx (unless listen_mode is enabled) if (!EMSESP_Settings.listen_mode) { regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS + dailyUpdatesTimer.attach(DAILYUPDATES_TIME, do_dailyUpdates); // daily reads from the EMS } // set timers for MQTT publish - // only if publish_time is not 0 (automatic mode) - if (EMSESP_Settings.publish_time) { - publishValuesTimer.attach(EMSESP_Settings.publish_time, do_publishValues); // post MQTT EMS values - publishSensorValuesTimer.attach(EMSESP_Settings.publish_time, publishSensorValues); // post MQTT dallas sensor values + if (EMSESP_Settings.publish_time > 0) { + publishValuesTimer.attach(EMSESP_Settings.publish_time, do_publishValues); // post MQTT EMS values + } else if (EMSESP_Settings.publish_time == 0) { + // automatic mode. use this Ticker to send out sensor values only. the EMS ones are done in the loop. + publishValuesTimer.attach(DEFAULT_SENSOR_PUBLISHTIME, publishSensorValues); } // set pin for LED @@ -1905,10 +2648,12 @@ void setup() { ledcheckTimer.attach_ms(LEDCHECK_TIME, do_ledcheck); // blink heartbeat LED } - // check for Dallas sensors - EMSESP_Settings.dallas_sensors = ds18.setup(EMSESP_Settings.dallas_gpio, EMSESP_Settings.dallas_parasite); // returns #sensors - + // system check systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if EMS is reachable + + // check for Dallas sensors + ds18.setup(EMSESP_Settings.dallas_gpio, EMSESP_Settings.dallas_parasite); + scanDallas(); } // @@ -1917,24 +2662,30 @@ void setup() { void loop() { myESP.loop(); // handle telnet, mqtt, wifi etc - // check Dallas sensors, using same schedule as publish_time (default 2 mins in DS18_READ_INTERVAL) - // these values are published to MQTT separately via the timer publishSensorValuesTimer - if (EMSESP_Settings.dallas_sensors) { - ds18.loop(); + // get Dallas Sensor readings every 2 seconds + static uint32_t last_check = 0; + uint32_t time_now = millis(); + if (time_now - last_check > 2000) { + last_check = time_now; + if (EMSESP_Settings.dallas_sensors) { + ds18.loop(); + } } - // publish EMS data to MQTT - // because of the force=false argument, it will see if there is anything received that must be published - publishEMSValues(false); - - // if we have an EMS connect go and fetch some data and MQTT publish it + // if we just have an EMS bus connection go and fetch the data and MQTT publish it to get started if (_need_first_publish) { - publishSensorValues(); - _need_first_publish = false; // reset flag + publishValues(true, true); + _need_first_publish = false; + return; } - // do shower logic, if enabled - if (EMSESP_Settings.shower_timer) { - showerCheck(); + // check if we're on auto mode for publishing + // then send EMS values, only if its been flagged to update. + // Don't send sensor data as this is done by the Ticker + if (EMSESP_Settings.publish_time == 0) { + publishValues(false, false); } + + // do shower logic + showerCheck(); } diff --git a/src/ems.cpp b/src/ems.cpp index fe9416bab..1cb335a5e 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -27,11 +27,11 @@ uint8_t _EMS_Devices_max = ArraySize(EMS_Devices); uint8_t _EMS_Devices_Types_max = ArraySize(EMS_Devices_Types); // 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_Mixing EMS_Mixing; // for mixing 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_MixingModule EMS_MixingModule; // for mixing 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, @@ -49,7 +49,7 @@ const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5, 0xF7, 0xE9, 0xEB, 0xED, 0xEF, 0xE1, 0xE3, 0xE5, 0xE7}; const uint8_t TX_WRITE_TIMEOUT_COUNT = 2; // 3 retries before timeout -const uint32_t EMS_BUS_TIMEOUT = 15000; // timeout in ms before recognizing the ems bus is offline (15 seconds) +const uint32_t EMS_BUS_TIMEOUT = 45000; // timeout in ms before recognizing the ems bus is offline (45 seconds) const uint32_t EMS_POLL_TIMEOUT = 5000000; // timeout in microseconds before recognizing the ems bus is offline (5 seconds) /* @@ -71,9 +71,14 @@ void ems_Device_remove_flags(unsigned int flags) { EMS_Sys_Status.emsRefreshedFlags &= ~flags; } -// init stats and counters and buffers +// returns true if HT3, other Buderus protocol +bool ems_isHT3() { + return (EMS_Sys_Status.emsIDMask == 0x80); +} + +// init EMS device values, counters and buffers void ems_init() { - ems_clearDeviceList(); // init the device map + Devices.clear(); // init the device map // overall status EMS_Sys_Status.emsRxPgks = 0; @@ -90,19 +95,29 @@ void ems_init() { EMS_Sys_Status.emsPollFrequency = 0; EMS_Sys_Status.txRetryCount = 0; EMS_Sys_Status.emsIDMask = 0x00; - EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME; + EMS_Sys_Status.emsTxMode = EMS_TXMODE_DEFAULT; + EMS_Sys_Status.emsbusid = EMS_BUSID_DEFAULT; + EMS_Sys_Status.emsPollAck[0] = EMS_BUSID_DEFAULT; // thermostat strlcpy(EMS_Thermostat.datetime, "?", sizeof(EMS_Thermostat.datetime)); EMS_Thermostat.write_supported = false; EMS_Thermostat.device_id = EMS_ID_NONE; + // settings + EMS_Thermostat.ibaMainDisplay = + EMS_VALUE_INT_NOTSET; // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 boiler temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 9 smoke temp + EMS_Thermostat.ibaLanguage = EMS_VALUE_INT_NOTSET; // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian + EMS_Thermostat.ibaCalIntTemperature = EMS_VALUE_INT_NOTSET; // offset int. temperature sensor, by * 0.1 Kelvin + EMS_Thermostat.ibaMinExtTemperature = EMS_VALUE_SHORT_NOTSET; // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 + EMS_Thermostat.ibaBuildingType = EMS_VALUE_INT_NOTSET; // building type: 0 = light, 1 = medium, 2 = heavy + EMS_Thermostat.ibaClockOffset = EMS_VALUE_INT_NOTSET; // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s // init all heating circuits for (uint8_t i = 0; i < EMS_THERMOSTAT_MAXHC; i++) { EMS_Thermostat.hc[i].hc = i + 1; EMS_Thermostat.hc[i].active = false; EMS_Thermostat.hc[i].mode = EMS_VALUE_INT_NOTSET; - EMS_Thermostat.hc[i].day_mode = EMS_VALUE_INT_NOTSET; + EMS_Thermostat.hc[i].mode_type = EMS_VALUE_INT_NOTSET; EMS_Thermostat.hc[i].summer_mode = EMS_VALUE_INT_NOTSET; EMS_Thermostat.hc[i].holiday_mode = EMS_VALUE_INT_NOTSET; EMS_Thermostat.hc[i].daytemp = EMS_VALUE_INT_NOTSET; @@ -114,53 +129,78 @@ void ems_init() { EMS_Thermostat.hc[i].curr_roomTemp = EMS_VALUE_SHORT_NOTSET; } - EMS_Mixing.detected = false; + EMS_MixingModule.device_id = EMS_ID_NONE; // init all mixing modules - for (uint8_t i = 0; i < EMS_THERMOSTAT_MAXHC; i++) { - EMS_Mixing.hc[i].hc = i + 1; - EMS_Mixing.hc[i].flowTemp = EMS_VALUE_SHORT_NOTSET; - EMS_Mixing.hc[i].pumpMod = EMS_VALUE_INT_NOTSET; - EMS_Mixing.hc[i].valveStatus = EMS_VALUE_INT_NOTSET; + for (uint8_t i = 0; i < EMS_MIXING_MAXHC; i++) { + EMS_MixingModule.hc[i].hc = i + 1; + EMS_MixingModule.hc[i].active = false; + EMS_MixingModule.hc[i].flowTemp = EMS_VALUE_USHORT_NOTSET; + EMS_MixingModule.hc[i].pumpMod = EMS_VALUE_INT_NOTSET; + EMS_MixingModule.hc[i].valveStatus = EMS_VALUE_INT_NOTSET; + EMS_MixingModule.hc[i].flowSetTemp = EMS_VALUE_INT_NOTSET; + } + + // init ww circuits + for (uint8_t i = 0; i < EMS_MIXING_MAXWWC; i++) { + EMS_MixingModule.wwc[i].wwc = i + 1; + EMS_MixingModule.wwc[i].active = false; + EMS_MixingModule.wwc[i].flowTemp = EMS_VALUE_USHORT_NOTSET; + EMS_MixingModule.wwc[i].pumpMod = EMS_VALUE_INT_NOTSET; + EMS_MixingModule.wwc[i].tempStatus = EMS_VALUE_INT_NOTSET; } // UBAParameterWW - EMS_Boiler.wWActivated = EMS_VALUE_BOOL_NOTSET; // Warm Water activated - EMS_Boiler.wWSelTemp = EMS_VALUE_INT_NOTSET; // Warm Water selected temperature - EMS_Boiler.wWCircPump = EMS_VALUE_BOOL_NOTSET; // Warm Water circulation pump available - EMS_Boiler.wWDesiredTemp = EMS_VALUE_INT_NOTSET; // Warm Water desired temperature to prevent infection - EMS_Boiler.wWComfort = EMS_VALUE_INT_NOTSET; // WW comfort mode + EMS_Boiler.wWActivated = EMS_VALUE_BOOL_NOTSET; // Warm Water activated + EMS_Boiler.wWSelTemp = EMS_VALUE_INT_NOTSET; // Warm Water selected temperature + EMS_Boiler.wWCircPump = EMS_VALUE_BOOL_NOTSET; // Warm Water circulation pump available + EMS_Boiler.wWCircPumpMode = EMS_VALUE_INT_NOTSET; // Warm Water circulation pump mode + EMS_Boiler.wWCircPumpType = EMS_VALUE_BOOL_NOTSET; // Warm Water circulation pump type + EMS_Boiler.wWDesinfectTemp = EMS_VALUE_INT_NOTSET; // Warm Water desinfection temperature to prevent infection + EMS_Boiler.wWComfort = EMS_VALUE_INT_NOTSET; // WW comfort mode - // UBAMonitorFast - 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_BOOL_NOTSET; // Gas on/off - EMS_Boiler.fanWork = EMS_VALUE_BOOL_NOTSET; // Fan on/off - EMS_Boiler.ignWork = EMS_VALUE_BOOL_NOTSET; // Ignition on/off - EMS_Boiler.heatPmp = EMS_VALUE_BOOL_NOTSET; // Boiler pump on/off - EMS_Boiler.wWHeat = EMS_VALUE_INT_NOTSET; // 3-way valve on WW - EMS_Boiler.wWCirc = EMS_VALUE_BOOL_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_USHORT_NOTSET; // Flame current in micro amps - EMS_Boiler.sysPress = EMS_VALUE_INT_NOTSET; // System pressure + // UBAMonitorFast - 0x18 on EMS1 + EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow temperature + EMS_Boiler.curFlowTemp = EMS_VALUE_USHORT_NOTSET; // Current flow temperature + EMS_Boiler.wwStorageTemp1 = EMS_VALUE_USHORT_NOTSET; // warm water storage temp 1 + EMS_Boiler.wwStorageTemp2 = EMS_VALUE_USHORT_NOTSET; // warm water storage temp 2 + EMS_Boiler.retTemp = EMS_VALUE_USHORT_NOTSET; // Return temperature + EMS_Boiler.burnGas = EMS_VALUE_BOOL_NOTSET; // Gas on/off + EMS_Boiler.wWMode = EMS_VALUE_BOOL_NOTSET; // Warm Water mode + EMS_Boiler.fanWork = EMS_VALUE_BOOL_NOTSET; // Fan on/off + EMS_Boiler.ignWork = EMS_VALUE_BOOL_NOTSET; // Ignition on/off + EMS_Boiler.heatPmp = EMS_VALUE_BOOL_NOTSET; // Boiler pump on/off + EMS_Boiler.wWHeat = EMS_VALUE_INT_NOTSET; // 3-way valve on WW + EMS_Boiler.wWCirc = EMS_VALUE_BOOL_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_USHORT_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_USHORT_NOTSET; - // UBAMonitorSlow + // UBAMonitorSlow - 0x19 on EMS1 EMS_Boiler.extTemp = EMS_VALUE_SHORT_NOTSET; // Outside temperature EMS_Boiler.boilTemp = EMS_VALUE_USHORT_NOTSET; // Boiler temperature + EMS_Boiler.exhaustTemp = EMS_VALUE_USHORT_NOTSET; // Exhaust 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.switchTemp = EMS_VALUE_USHORT_NOTSET; // UBAMonitorWWMessage - 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; // WW current flow temp + EMS_Boiler.wWSetTmp = EMS_VALUE_INT_NOTSET; // Warm Water set temperature + EMS_Boiler.wWCurTmp = EMS_VALUE_USHORT_NOTSET; // Warm Water current temperature + EMS_Boiler.wWCurTmp2 = EMS_VALUE_USHORT_NOTSET; // Warm Water current temperature storage + 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.wWDesinfecting = EMS_VALUE_INT_NOTSET; // Warm Water desinfection on/off + EMS_Boiler.wWReadiness = EMS_VALUE_INT_NOTSET; // Warm Water readiness on/off + EMS_Boiler.wWRecharging = EMS_VALUE_INT_NOTSET; // Warm Water recharge on/off + EMS_Boiler.wWTemperatureOK = EMS_VALUE_INT_NOTSET; // Warm Water temperature ok on/off + + EMS_Boiler.wWCurFlow = EMS_VALUE_INT_NOTSET; // WW current flow temp // UBATotalUptimeMessage EMS_Boiler.UBAuptime = EMS_VALUE_LONG_NOTSET; // Total UBA working hours @@ -171,13 +211,15 @@ void ems_init() { 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.collectorTemp = EMS_VALUE_SHORT_NOTSET; // collector temp from SM10/SM100/SM200 + EMS_SolarModule.bottomTemp = EMS_VALUE_SHORT_NOTSET; // bottom temp from SM10/SM100/SM200 + EMS_SolarModule.bottomTemp2 = EMS_VALUE_SHORT_NOTSET; // bottom temp 2 from SM200 + EMS_SolarModule.pumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10/SM100/SM200 EMS_SolarModule.pump = EMS_VALUE_BOOL_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.valveStatus = EMS_VALUE_BOOL_NOTSET; // valve status from SM200 + EMS_SolarModule.EnergyLastHour = EMS_VALUE_LONG_NOTSET; + EMS_SolarModule.EnergyToday = EMS_VALUE_LONG_NOTSET; + EMS_SolarModule.EnergyTotal = EMS_VALUE_LONG_NOTSET; EMS_SolarModule.device_id = EMS_ID_NONE; EMS_SolarModule.product_id = EMS_ID_NONE; EMS_SolarModule.pumpWorkMin = EMS_VALUE_LONG_NOTSET; @@ -202,7 +244,7 @@ void ems_init() { strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version)); // default logging is none - ems_setLogging(EMS_SYS_LOGGING_DEFAULT); + ems_setLogging(EMS_SYS_LOGGING_DEFAULT, true); } // Getters and Setters for parameters @@ -223,8 +265,8 @@ bool ems_getThermostatEnabled() { return (EMS_Thermostat.device_id != EMS_ID_NONE); } -bool ems_getMixingDeviceEnabled() { - return EMS_Mixing.detected; +bool ems_getMixingModuleEnabled() { + return EMS_MixingModule.device_id != EMS_ID_NONE; } bool ems_getSolarModuleEnabled() { @@ -235,11 +277,12 @@ bool ems_getHeatPumpEnabled() { return (EMS_HeatPump.device_id != EMS_ID_NONE); } -uint8_t ems_getThermostatModel() { - return (EMS_Thermostat.device_flags & 0x7F); // strip 7th bit +// thermostat has already the 7th bit (to indicate write) stripped +uint8_t ems_getThermostatFlags() { + return (EMS_Thermostat.device_flags); } -uint8_t ems_getSolarModuleModel() { +uint8_t ems_getSolarModuleFlags() { return (EMS_SolarModule.device_flags); } @@ -273,28 +316,38 @@ _EMS_SYS_LOGGING ems_getLogging() { return EMS_Sys_Status.emsLogging; } -void ems_setLogging(_EMS_SYS_LOGGING loglevel, uint16_t type_id) { - if (loglevel <= EMS_SYS_LOGGING_JABBER) { - EMS_Sys_Status.emsLogging = loglevel; +void ems_setLogging(_EMS_SYS_LOGGING loglevel, uint16_t id) { + EMS_Sys_Status.emsLogging_ID = id; + ems_setLogging(loglevel, false); +} - if (loglevel == EMS_SYS_LOGGING_NONE) { - myDebug_P(PSTR("System Logging set to None")); - } else if (loglevel == EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("System Logging set to Basic")); - } else if (loglevel == EMS_SYS_LOGGING_VERBOSE) { - 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")); - } else if (loglevel == EMS_SYS_LOGGING_JABBER) { - myDebug_P(PSTR("System Logging set to Jabber mode")); - } else if (loglevel == EMS_SYS_LOGGING_WATCH) { - EMS_Sys_Status.emsLogging_typeID = type_id; - myDebug_P(PSTR("System Logging set to Watch mode")); - } +void ems_setLogging(_EMS_SYS_LOGGING loglevel, bool quiet) { + EMS_Sys_Status.emsLogging = loglevel; + + if (quiet) { + return; // no reporting to console + } + + if (loglevel == EMS_SYS_LOGGING_NONE) { + myDebug_P(PSTR("System Logging set to None")); + } else if (loglevel == EMS_SYS_LOGGING_BASIC) { + myDebug_P(PSTR("System Logging set to Basic")); + } else if (loglevel == EMS_SYS_LOGGING_VERBOSE) { + 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_MIXINGMODULE) { + myDebug_P(PSTR("System Logging set to Mixing Module only")); + } else if (loglevel == EMS_SYS_LOGGING_RAW) { + myDebug_P(PSTR("System Logging set to Raw mode")); + } else if (loglevel == EMS_SYS_LOGGING_JABBER) { + myDebug_P(PSTR("System Logging set to Jabber mode")); + } else if (loglevel == EMS_SYS_LOGGING_WATCH) { + myDebug_P(PSTR("System Logging set to Watch mode")); + } else if (loglevel == EMS_SYS_LOGGING_DEVICE) { + myDebug_P(PSTR("System Logging set to Device mode")); } } @@ -325,78 +378,124 @@ uint8_t _crcCalculator(uint8_t * data, uint8_t len) { return crc; } +// validate we have data at the offset (index) requested +// returns -1 if out of bounds +int8_t _getDataPosition(_EMS_RxTelegram * EMS_RxTelegram, uint8_t index) { + int8_t pos = index - EMS_RxTelegram->offset; // get adjusted index position based on offset + return (pos >= EMS_RxTelegram->data_length) ? -1 : pos; // return -1 if out of bounds +} + // unsigned short -void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint16_t * param_op, uint8_t index) { - if (index >= EMS_RxTelegram->data_length) { - return; +bool _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint16_t * param_op, uint8_t index) { + int8_t pos = _getDataPosition(EMS_RxTelegram, index); + if (pos < 0) { + return false; } - uint16_t value = (EMS_RxTelegram->data[index] << 8) + EMS_RxTelegram->data[index + 1]; + uint16_t value = (EMS_RxTelegram->data[pos] << 8) + EMS_RxTelegram->data[pos + 1]; - // check for undefined/unset values, 0x8000 - if (value == EMS_VALUE_USHORT_NOTSET) { - return; + // check for undefined/unset values, 0x8000, 0x8300, 0x7D00 + if ((value == EMS_VALUE_USHORT_NOTSET) || (value == EMS_VALUE_SHORT_NOTSET) || (value == EMS_VALUE_USHORT_NOTVALID)) { + *param_op = EMS_VALUE_USHORT_NOTSET; // make sure we render this right + return false; } *param_op = value; + return true; } // signed short -void _setValue(_EMS_RxTelegram * EMS_RxTelegram, int16_t * param_op, uint8_t index) { - if (index >= EMS_RxTelegram->data_length) { - return; +bool _setValue(_EMS_RxTelegram * EMS_RxTelegram, int16_t * param_op, uint8_t index) { + int8_t pos = _getDataPosition(EMS_RxTelegram, index); + if (pos < 0) { + return false; } - int16_t value = (EMS_RxTelegram->data[index] << 8) + EMS_RxTelegram->data[index + 1]; + int16_t value = (EMS_RxTelegram->data[pos] << 8) + EMS_RxTelegram->data[pos + 1]; - // check for undefined/unset values, 0x8000 - if ((value == EMS_VALUE_SHORT_NOTSET) || (EMS_RxTelegram->data[index] == 0x7D)) { - return; + // check for undefined/unset values, 0x8000, 0x8300, 0x7D00 + if ((value == EMS_VALUE_USHORT_NOTSET) || (value == EMS_VALUE_SHORT_NOTSET) || (value == EMS_VALUE_USHORT_NOTVALID)) { + *param_op = EMS_VALUE_SHORT_NOTSET; // make sure we render this right + return false; } *param_op = value; + return true; } // Byte -void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint8_t * param_op, uint8_t index) { - if (index >= EMS_RxTelegram->data_length) { - return; +bool _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint8_t * param_op, uint8_t index) { + int8_t pos = _getDataPosition(EMS_RxTelegram, index); + if (pos < 0) { + return false; } - *param_op = (uint8_t)EMS_RxTelegram->data[index]; + *param_op = (uint8_t)EMS_RxTelegram->data[pos]; + return true; } // convert signed short to single 8 byte, for setpoint thermostat temperatures that don't store their temps in 2 bytes -void _setValue8(_EMS_RxTelegram * EMS_RxTelegram, int16_t * param_op, uint8_t index) { - if (index >= EMS_RxTelegram->data_length) { - return; +bool _setValue8(_EMS_RxTelegram * EMS_RxTelegram, int16_t * param_op, uint8_t index) { + int8_t pos = _getDataPosition(EMS_RxTelegram, index); + if (pos < 0) { + return false; } - *param_op = EMS_RxTelegram->data[index]; + *param_op = EMS_RxTelegram->data[pos]; + return true; } -// Long -void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint32_t * param_op, uint8_t index) { - if (index >= EMS_RxTelegram->data_length) { - return; +// Long 24 bit +bool _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint32_t * param_op, uint8_t index) { + int8_t pos = _getDataPosition(EMS_RxTelegram, index); + if (pos < 0) { + return false; } - *param_op = (uint32_t)((EMS_RxTelegram->data[index] << 16) + (EMS_RxTelegram->data[index + 1] << 8) + (EMS_RxTelegram->data[index + 2])); + *param_op = (uint32_t)((EMS_RxTelegram->data[pos] << 16) + (EMS_RxTelegram->data[pos + 1] << 8) + (EMS_RxTelegram->data[pos + 2])); + return true; +} + +// Long 32 bit +bool _setValue32(_EMS_RxTelegram * EMS_RxTelegram, uint32_t * param_op, uint8_t index) { + int8_t pos = _getDataPosition(EMS_RxTelegram, index); + if (pos < 0) { + return false; + } + + *param_op = (uint32_t)((EMS_RxTelegram->data[pos] << 24) + (EMS_RxTelegram->data[pos + 1] << 16) + (EMS_RxTelegram->data[pos + 2] << 8) + + (EMS_RxTelegram->data[pos + 3])); + return true; } // bit from a byte -void _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint8_t * param_op, uint8_t index, uint8_t bit) { - if (index >= EMS_RxTelegram->data_length) { - return; +bool _setValue(_EMS_RxTelegram * EMS_RxTelegram, uint8_t * param_op, uint8_t index, uint8_t bit) { + int8_t pos = _getDataPosition(EMS_RxTelegram, index); + if (pos < 0) { + return false; } - *param_op = (uint8_t)(((EMS_RxTelegram->data[index]) >> (bit)) & 0x01); + *param_op = (uint8_t)(((EMS_RxTelegram->data[pos]) >> (bit)) & 0x01); + return true; } void ems_setTxMode(uint8_t mode) { EMS_Sys_Status.emsTxMode = mode; } +void ems_setEMSbusid(uint8_t id) { + if ((id != 0x0B) && (id != 0x0A) && (id != 0x0D) && (id != 0x0F) && (id != 0x12)) { + id = EMS_BUSID_DEFAULT; + } + + EMS_Sys_Status.emsbusid = id; + EMS_Sys_Status.emsPollAck[0] = id; +} + +void ems_setMasterThermostat(uint8_t product_id) { + EMS_Sys_Status.emsMasterThermostat = product_id; +} + /** * debug print a telegram to telnet/serial including the CRC */ @@ -443,14 +542,17 @@ void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, strlcat(output_str, _smallitoa3(t_msec, buffer), sizeof(output_str)); } - if (!raw) + if (!raw) { strlcat(output_str, COLOR_RESET, sizeof(output_str)); + } strlcat(output_str, ") ", sizeof(output_str)); - if (!raw) + if (!raw) { strlcat(output_str, color, sizeof(output_str)); + } + // add the header strlcat(output_str, prefix, sizeof(output_str)); if (!raw) { @@ -463,14 +565,15 @@ void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, } if (!raw) { - strlcat(output_str, "(CRC=", sizeof(output_str)); - strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str)); - strlcat(output_str, ")", sizeof(output_str)); + //strlcat(output_str, "(CRC=", sizeof(output_str)); + //strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str)); + //strlcat(output_str, ")", sizeof(output_str)); // print number of data bytes only if its a valid telegram if (data_len) { - strlcat(output_str, " #data=", sizeof(output_str)); + strlcat(output_str, "(#data=", sizeof(output_str)); strlcat(output_str, itoa(data_len, buffer, 10), sizeof(output_str)); + strlcat(output_str, ")", sizeof(output_str)); } strlcat(output_str, COLOR_RESET, sizeof(output_str)); @@ -510,7 +613,7 @@ void _ems_sendTelegram() { _EMS_RxTelegram EMS_RxTelegram; // create new Rx object EMS_RxTelegram.length = EMS_TxTelegram.length; // full length of telegram EMS_RxTelegram.telegram = EMS_TxTelegram.data; - EMS_RxTelegram.data_length = 0; // ignore #data= + EMS_RxTelegram.data_length = 0; // surpress #data= EMS_RxTelegram.timestamp = myESP.getSystemTime(); // now _debugPrintTelegram("Sending raw: ", &EMS_RxTelegram, COLOR_CYAN, true); } @@ -528,7 +631,7 @@ void _ems_sendTelegram() { } // create the header - EMS_TxTelegram.data[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; // src + EMS_TxTelegram.data[0] = EMS_Sys_Status.emsbusid ^ EMS_Sys_Status.emsIDMask; // src // dest if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { @@ -539,7 +642,8 @@ void _ems_sendTelegram() { } // complete the rest of the header depending on EMS or EMS+ - if (EMS_TxTelegram.type > 0xFF) { + // or if we explicitly set EMS+ like with Junkers + if ((EMS_TxTelegram.type > 0xFF) || (EMS_TxTelegram.emsplus)) { // EMS 2.0 / EMS+ EMS_TxTelegram.data[2] = 0xFF; // fixed value indicating an extended message EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; @@ -587,7 +691,7 @@ void _ems_sendTelegram() { } // send the telegram to the UART Tx - _EMS_TX_STATUS _txStatus = emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx + _EMS_TX_STATUS _txStatus = emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); if (EMS_TX_STATUS_OK == _txStatus || EMS_TX_STATUS_IDLE == _txStatus) EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_WAIT; else { @@ -631,6 +735,7 @@ void _createValidate() { new_EMS_TxTelegram.comparisonValue = EMS_TxTelegram.comparisonValue; new_EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.comparisonPostRead; new_EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.comparisonOffset; + new_EMS_TxTelegram.emsplus = EMS_TxTelegram.emsplus; // this is what is different new_EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // location of byte to fetch @@ -703,7 +808,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { if ((length >= 5) && (telegram[length - 1] == _crcCalculator(telegram, length))) { EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; EMS_Sys_Status.emsIDMask = telegram[0] & 0x80; - EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; + EMS_Sys_Status.emsPollAck[0] = EMS_Sys_Status.emsbusid ^ EMS_Sys_Status.emsIDMask; } else return; // ignore the whole telegram Rx Telegram while in DETECT mode } @@ -712,12 +817,14 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { * It may happen that we were interrupted (for instance 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("** Warning, we missed the bus - Rx non-idle!")); } return; } + */ /* * check if we just received one byte @@ -725,14 +832,14 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { * 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 - static uint32_t _last_emsPollFrequency = 0; + uint8_t value = telegram[0]; // 1st byte of data package // check first for a Poll for us - if ((value ^ 0x80 ^ EMS_Sys_Status.emsIDMask) == EMS_ID_ME) { - uint32_t timenow_microsecs = micros(); - EMS_Sys_Status.emsPollFrequency = (timenow_microsecs - _last_emsPollFrequency); - _last_emsPollFrequency = timenow_microsecs; + if ((value ^ 0x80 ^ EMS_Sys_Status.emsIDMask) == EMS_Sys_Status.emsbusid) { + static uint32_t _last_emsPollFrequency = 0; + uint32_t timenow_microsecs = micros(); + EMS_Sys_Status.emsPollFrequency = (timenow_microsecs - _last_emsPollFrequency); + _last_emsPollFrequency = timenow_microsecs; // do we have something to send thats waiting in the Tx queue? // if so send it if the Queue is not in a wait state @@ -819,8 +926,11 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // but still continue to process it if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW)) { _debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true); - } else if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_WATCH) && (EMS_RxTelegram.type == EMS_Sys_Status.emsLogging_typeID)) { + } else if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_WATCH) && (EMS_RxTelegram.type == EMS_Sys_Status.emsLogging_ID)) { _debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true); + // raw printout for log d [id] disabled, moved to _printMessage() + // } else if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_DEVICE) && ((EMS_RxTelegram.src == EMS_Sys_Status.emsLogging_ID) || (EMS_RxTelegram.dest == EMS_Sys_Status.emsLogging_ID))) { + // _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] @@ -842,68 +952,6 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { _processType(&EMS_RxTelegram); } -/** - * print the telegram - */ -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 length = EMS_RxTelegram->data_length; - - char output_str[200] = {0}; - char buffer[16] = {0}; - char color_s[20] = {0}; - char type_s[30]; - - // source - ems_getDeviceTypeDescription(src, type_s); - strlcpy(output_str, type_s, sizeof(output_str)); - strlcat(output_str, " -> ", sizeof(output_str)); - - // destination - (void)ems_getDeviceTypeDescription(dest, type_s); - strlcat(output_str, type_s, sizeof(output_str)); - - if (dest == EMS_ID_ME) { - strlcpy(color_s, COLOR_YELLOW, sizeof(color_s)); // me - } else if (dest == EMS_ID_NONE) { - strlcpy(color_s, COLOR_GREEN, sizeof(color_s)); // broadcast - } else { - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); // everything else - } - - if (length) { - // 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)); - } - } - - 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); - } -} - /** * Remove current Tx telegram from queue and release lock on Tx */ @@ -916,7 +964,7 @@ void _removeTxQueue() { /** * Check if hot tap water or heating is active - * using a quick hack for checking the heating. Selected Flow Temp >= 70 + * using a quick hack for checking the heating by looking at the Selected Flow Temp */ void _checkActive() { // hot tap water, using flow to check instead of the burner power @@ -935,10 +983,12 @@ void _checkActive() { * received only after requested (not broadcasted) */ void _process_UBAParameterWW(_EMS_RxTelegram * EMS_RxTelegram) { - _setValue(EMS_RxTelegram, &EMS_Boiler.wWActivated, 1); // 0xFF means on - _setValue(EMS_RxTelegram, &EMS_Boiler.wWCircPump, 6); // 0xFF means on + _setValue(EMS_RxTelegram, &EMS_Boiler.wWActivated, 1); // 0xFF means on + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCircPump, 6); // 0xFF means on + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCircPumpMode, 7); // 1=1x3min... 6=6x3min, 7=continuous + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCircPumpType, 10); // 0 = charge pump, 0xff = 3-way valve _setValue(EMS_RxTelegram, &EMS_Boiler.wWSelTemp, 2); - _setValue(EMS_RxTelegram, &EMS_Boiler.wWDesiredTemp, 8); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWDesinfectTemp, 8); _setValue(EMS_RxTelegram, &EMS_Boiler.wWComfort, EMS_OFFSET_UBAParameterWW_wwComfort); } @@ -964,16 +1014,23 @@ void _process_UBAParametersMessage(_EMS_RxTelegram * EMS_RxTelegram) { * received every 10 seconds */ void _process_UBAMonitorWWMessage(_EMS_RxTelegram * EMS_RxTelegram) { + _setValue(EMS_RxTelegram, &EMS_Boiler.wWSetTmp, 0); _setValue(EMS_RxTelegram, &EMS_Boiler.wWCurTmp, 1); - _setValue(EMS_RxTelegram, &EMS_Boiler.wWStarts, 13); - _setValue(EMS_RxTelegram, &EMS_Boiler.wWWorkM, 10); - _setValue(EMS_RxTelegram, &EMS_Boiler.wWOneTime, 5, 1); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWCurTmp2, 3); _setValue(EMS_RxTelegram, &EMS_Boiler.wWCurFlow, 9); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWWorkM, 10); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWStarts, 13); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWOneTime, 5, 1); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWDesinfecting, 5, 2); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWReadiness, 5, 3); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWRecharging, 5, 4); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWTemperatureOK, 5, 5); } /** * Activate / De-activate One Time warm water 0x35 * true = on, false = off + * See also https://github.com/proddy/EMS-ESP/issues/341#issuecomment-596245458 for Junkers */ void ems_setWarmWaterOnetime(bool activated) { myDebug_P(PSTR("Setting boiler warm water OneTime loading %s"), activated ? "on" : "off"); @@ -985,13 +1042,34 @@ void ems_setWarmWaterOnetime(bool activated) { EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; EMS_TxTelegram.dest = EMS_Boiler.device_id; EMS_TxTelegram.type = EMS_TYPE_UBAFlags; - EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwOneTime; + EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwOneTime; // use offset 0x01 for external storage on Junkers EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate EMS_TxTelegram.dataValue = (activated ? 0x22 : 0x02); // 0x22 is on, 0x02 is off for RC20RF EMS_TxQueue.push(EMS_TxTelegram); } +/** + * Activate / De-activate circulation of warm water 0x35 + * true = on, false = off + */ +void ems_setWarmWaterCirculation(bool activated) { + myDebug_P(PSTR("Setting boiler warm water circulation %s"), activated ? "on" : "off"); + + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = EMS_Boiler.device_id; + EMS_TxTelegram.type = EMS_TYPE_UBAFlags; + EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwCirulation; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + EMS_TxTelegram.dataValue = (activated ? 0x22 : 0x02); + + EMS_TxQueue.push(EMS_TxTelegram); +} /** * UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long) @@ -1004,13 +1082,18 @@ void _process_UBAMonitorFast(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Boiler.curBurnPow, 4); _setValue(EMS_RxTelegram, &EMS_Boiler.burnGas, 7, 0); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWMode, 7, 1); _setValue(EMS_RxTelegram, &EMS_Boiler.fanWork, 7, 2); _setValue(EMS_RxTelegram, &EMS_Boiler.ignWork, 7, 3); _setValue(EMS_RxTelegram, &EMS_Boiler.heatPmp, 7, 5); _setValue(EMS_RxTelegram, &EMS_Boiler.wWHeat, 7, 6); _setValue(EMS_RxTelegram, &EMS_Boiler.wWCirc, 7, 7); - _setValue(EMS_RxTelegram, &EMS_Boiler.boilTemp, 11); // 0x8000 if not available + // warm water storage sensors (if present) + // wwStorageTemp2 is also used by some brands as the boiler temperature - see https://github.com/proddy/EMS-ESP/issues/206 + _setValue(EMS_RxTelegram, &EMS_Boiler.wwStorageTemp1, 9); // 0x8300 if not available + _setValue(EMS_RxTelegram, &EMS_Boiler.wwStorageTemp2, 11); // 0x8000 if not available - this is boiler temp + _setValue(EMS_RxTelegram, &EMS_Boiler.retTemp, 13); _setValue(EMS_RxTelegram, &EMS_Boiler.flameCurr, 15); _setValue(EMS_RxTelegram, &EMS_Boiler.serviceCode, 20); @@ -1019,7 +1102,7 @@ void _process_UBAMonitorFast(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Boiler.sysPress, 17); // is *10 // read the service code / installation status as appears on the display - if (EMS_RxTelegram->data_length > 18) { + if ((EMS_RxTelegram->data_length > 18) && (EMS_RxTelegram->offset == 0)) { EMS_Boiler.serviceCodeChar[0] = char(EMS_RxTelegram->data[18]); // ascii character 1 EMS_Boiler.serviceCodeChar[1] = char(EMS_RxTelegram->data[19]); // ascii character 2 EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string @@ -1030,11 +1113,12 @@ void _process_UBAMonitorFast(_EMS_RxTelegram * EMS_RxTelegram) { } /** - * UBAMonitorFast2 - type 0xE4 - central heating monitor + * UBAMonitorFast2 - type 0xE4 - central heating monitor EMS+ */ void _process_UBAMonitorFast2(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Boiler.selFlowTemp, 6); _setValue(EMS_RxTelegram, &EMS_Boiler.burnGas, 11, 0); + _setValue(EMS_RxTelegram, &EMS_Boiler.wWMode, 11, 1); _setValue(EMS_RxTelegram, &EMS_Boiler.wWHeat, 11, 2); _setValue(EMS_RxTelegram, &EMS_Boiler.curBurnPow, 10); _setValue(EMS_RxTelegram, &EMS_Boiler.selBurnPow, 9); @@ -1042,7 +1126,7 @@ void _process_UBAMonitorFast2(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Boiler.flameCurr, 19); // read the service code / installation status as appears on the display - if (EMS_RxTelegram->data_length > 4) { + if ((EMS_RxTelegram->data_length > 4) && (EMS_RxTelegram->offset == 0)) { EMS_Boiler.serviceCodeChar[0] = char(EMS_RxTelegram->data[4]); // ascii character 1 EMS_Boiler.serviceCodeChar[1] = char(EMS_RxTelegram->data[5]); // ascii character 2 EMS_Boiler.serviceCodeChar[2] = '\0'; @@ -1060,14 +1144,15 @@ void _process_UBAMonitorFast2(_EMS_RxTelegram * EMS_RxTelegram) { /** * UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long) * received every 60 seconds - * e.g. 08 00 19 00 80 00 02 41 80 00 00 00 00 00 03 91 7B 05 B8 40 00 00 00 04 92 AD 00 5E EE 80 00 (CRC=C9) #data=27 + * e.g. 08 00 19 00 80 00 02 41 80 00 00 00 00 00 03 91 7B 05 B8 40 00 00 00 04 92 AD 00 5E EE 80 00 * 08 0B 19 00 FF EA 02 47 80 00 00 00 00 62 03 CA 24 2C D6 23 00 00 00 27 4A B6 03 6E 43 * 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 17 19 20 21 22 23 24 */ void _process_UBAMonitorSlow(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Boiler.extTemp, 0); _setValue(EMS_RxTelegram, &EMS_Boiler.boilTemp, 2); - _setValue(EMS_RxTelegram, &EMS_Boiler.switchTemp, 25); // only if there is a mixer + _setValue(EMS_RxTelegram, &EMS_Boiler.exhaustTemp, 4); + _setValue(EMS_RxTelegram, &EMS_Boiler.switchTemp, 25); // only if there is a mixing module present _setValue(EMS_RxTelegram, &EMS_Boiler.pumpMod, 9); _setValue(EMS_RxTelegram, &EMS_Boiler.burnStarts, 10); _setValue(EMS_RxTelegram, &EMS_Boiler.burnWorkMin, 13); @@ -1075,7 +1160,7 @@ void _process_UBAMonitorSlow(_EMS_RxTelegram * EMS_RxTelegram) { } /** - * UBAMonitorSlow2 - type 0xE5 - central heating monitor + * UBAMonitorSlow2 - type 0xE5 - central heating monitor EMS+ */ void _process_UBAMonitorSlow2(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Boiler.fanWork, 2, 2); @@ -1089,7 +1174,7 @@ void _process_UBAMonitorSlow2(_EMS_RxTelegram * EMS_RxTelegram) { } /** - * UBAOutdoorTemp - type 0xD1 - external temperature + * UBAOutdoorTemp - type 0xD1 - external temperature EMS+ */ void _process_UBAOutdoorTemp(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Boiler.extTemp, 0); @@ -1099,7 +1184,7 @@ void _process_UBAOutdoorTemp(_EMS_RxTelegram * EMS_RxTelegram) { * type 0xB1 - data from the RC10 thermostat (0x17) * For reading the temp values only * received every 60 seconds - * e.g. 17 0B 91 00 80 1E 00 CB 27 00 00 00 00 05 01 00 CB 00 (CRC=47), #data=14 + * e.g. 17 0B 91 00 80 1E 00 CB 27 00 00 00 00 05 01 00 CB 00 */ void _process_RC10StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 @@ -1109,6 +1194,19 @@ void _process_RC10StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RC10StatusMessage_curr); // is * 10 } +/** + * type 0xAE - data from the RC20 thermostat (0x17) + * e.g. 17 00 AE 00 80 12 2E 00 D0 00 00 64 + */ +void _process_RC20NStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + EMS_Thermostat.hc[hc].active = true; + + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode_type, 1, 1); // day/night 1th bit is day + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, 2); // is * 2, force as single byte + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, 3); // is * 10 +} + /** * type 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long * For reading the temp values only @@ -1166,24 +1264,23 @@ void _process_RC30StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { void _process_RC35StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { // exit if... // - the 15th byte (second from last) is 0x00, which I think is flow temp, means HC is not is use - // - its not a broadcast, so destination is 0x00 - if ((EMS_RxTelegram->data[14] == 0x00) || (EMS_RxTelegram->dest != EMS_ID_NONE)) { + // - its not a broadcast, so destination is 0x00 (not in use since 6/1/2020 - issue #238) + // if ((EMS_RxTelegram->data[14] == 0x00) || (EMS_RxTelegram->dest != EMS_ID_NONE)) { + if (EMS_RxTelegram->data[14] == 0x00) { return; } - uint8_t hc_num = _getHeatingCircuit(EMS_RxTelegram); // which HC is it, 0-3 - - // ignore if the value is 0 (see https://github.com/proddy/EMS-ESP/commit/ccc30738c00f12ae6c89177113bd15af9826b836) - if (EMS_RxTelegram->data[EMS_OFFSET_RC35StatusMessage_setpoint] != 0x00) { - _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].setpoint_roomTemp, EMS_OFFSET_RC35StatusMessage_setpoint); // is * 2, force to single byte + int8_t hc = _getHeatingCircuit(EMS_RxTelegram); // which HC is it, 0-3 + if (hc == -1) { + return; } - // ignore if the value is unset. Hopefully it will be picked up via a later message - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].curr_roomTemp, EMS_OFFSET_RC35StatusMessage_curr); // is * 10 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].day_mode, EMS_OFFSET_RC35StatusMessage_mode, 1); - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].summer_mode, EMS_OFFSET_RC35StatusMessage_mode, 0); - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].holiday_mode, EMS_OFFSET_RC35StatusMessage_mode1, 5); - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].circuitcalctemp, EMS_OFFSET_RC35Set_circuitcalctemp); + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RC35StatusMessage_setpoint); // is * 2, force to single byte + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RC35StatusMessage_curr); // is * 10 - or 0x7D00 if thermostat is mounted on boiler + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode_type, EMS_OFFSET_RC35StatusMessage_mode, 1); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].summer_mode, EMS_OFFSET_RC35StatusMessage_mode, 0); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].holiday_mode, EMS_OFFSET_RC35StatusMessage_mode1, 5); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].circuitcalctemp, EMS_OFFSET_RC35Set_circuitcalctemp); } /** @@ -1198,16 +1295,73 @@ void _process_EasyStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_EasyStatusMessage_setpoint); // is * 100 } +// Settings Parameters - 0xA5 +void _process_IBASettingsMessage(_EMS_RxTelegram * EMS_RxTelegram) { + // thermostat compatible with this settings message, checked by in ems_getSettingsValues() + + // at init, and every 24h, send read request for settings parameters message, done with Ticker on do_dailyUpdates() + // send 0B 90 A5 00 20: + // 10 0B A5 00 00 02 00 00 FF F6 01 06 00 01 0D 03 03 + // 00 01 02 03 04 05 06 07 08 09 10 11 12 + + // values validated for RC30N + uint8_t extTemp = 100; // Min. ext temperature is coded as int8, 0xF6=-10, 0x0 = 0, 0xFF=-1. 100 is out of permissible range + + _setValue(EMS_RxTelegram, + &EMS_Thermostat.ibaMainDisplay, + EMS_OFFSET_IBASettings_Display); // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 9 smoke temp + _setValue(EMS_RxTelegram, &EMS_Thermostat.ibaLanguage, EMS_OFFSET_IBASettings_Language); // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian + _setValue(EMS_RxTelegram, &EMS_Thermostat.ibaBuildingType, EMS_OFFSET_IBASettings_Building); // building type: 0 = light, 1 = medium, 2 = heavy + _setValue(EMS_RxTelegram, &EMS_Thermostat.ibaCalIntTemperature, EMS_OFFSET_IBASettings_CalIntTemp); // offset int. temperature sensor, by * 0.1 Kelvin + _setValue(EMS_RxTelegram, &extTemp, EMS_OFFSET_IBASettings_MinExtTemp); // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 + if (extTemp != 100) { + // code as signed short, to benefit from negative value rendering + EMS_Thermostat.ibaMinExtTemperature = (int16_t)(extTemp > 127) ? (extTemp - 256) : extTemp; + } + _setValue(EMS_RxTelegram, &EMS_Thermostat.ibaClockOffset, EMS_OFFSET_IBASettings_ClockOffset); // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s + + // publish settings to mqtt (assuming this is a very low frequency message), done in publishEMSValues_settings() + ems_Device_add_flags(EMS_DEVICE_UPDATE_FLAG_SETTINGS); +} + +// Mixing module - 0x01D7, 0x01D8 void _process_MMPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t hc = (EMS_RxTelegram->type - EMS_TYPE_MMPLUSStatusMessage_HC1); // 0 to 3 - if (hc >= EMS_THERMOSTAT_MAXHC) { + if (hc >= EMS_MIXING_MAXHC) { return; // invalid type } - EMS_Mixing.hc[hc].active = true; + EMS_MixingModule.hc[hc].active = true; - _setValue(EMS_RxTelegram, &EMS_Mixing.hc[hc].flowTemp, EMS_OFFSET_MMPLUSStatusMessage_flow_temp); - _setValue(EMS_RxTelegram, &EMS_Mixing.hc[hc].pumpMod, EMS_OFFSET_MMPLUSStatusMessage_pump_mod); - _setValue(EMS_RxTelegram, &EMS_Mixing.hc[hc].valveStatus, EMS_OFFSET_MMPLUSStatusMessage_valve_status); + _setValue(EMS_RxTelegram, &EMS_MixingModule.hc[hc].flowTemp, EMS_OFFSET_MMPLUSStatusMessage_flow_temp); + _setValue(EMS_RxTelegram, &EMS_MixingModule.hc[hc].pumpMod, EMS_OFFSET_MMPLUSStatusMessage_pump_mod); + _setValue(EMS_RxTelegram, &EMS_MixingModule.hc[hc].valveStatus, EMS_OFFSET_MMPLUSStatusMessage_valve_status); +} + +// Mixing module warm water loading - 0x0231, 0x0232 +void _process_MMPLUSStatusMessageWW(_EMS_RxTelegram * EMS_RxTelegram) { + uint8_t wwc = (EMS_RxTelegram->type - EMS_TYPE_MMPLUSStatusMessage_WWC1); // 0 to 1 + if (wwc >= EMS_MIXING_MAXWWC) { + return; // invalid type + } + EMS_MixingModule.wwc[wwc].active = true; + + _setValue(EMS_RxTelegram, &EMS_MixingModule.wwc[wwc].flowTemp, EMS_OFFSET_MMPLUSStatusMessage_WW_flow_temp); + _setValue(EMS_RxTelegram, &EMS_MixingModule.wwc[wwc].pumpMod, EMS_OFFSET_MMPLUSStatusMessage_WW_pump_mod); + _setValue(EMS_RxTelegram, &EMS_MixingModule.wwc[wwc].tempStatus, EMS_OFFSET_MMPLUSStatusMessage_WW_temp_status); +} + +// Mixing - 0xAB +// https://github.com/proddy/EMS-ESP/issues/270 +// We assume MM10 is on HC2 and WM10 is using HC1 +void _process_MMStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { + uint8_t hc = 1; // fixed to HC2 + EMS_MixingModule.hc[hc].active = true; + + _setValue(EMS_RxTelegram, &EMS_MixingModule.hc[hc].flowTemp, EMS_OFFSET_MMStatusMessage_flow_temp); + _setValue(EMS_RxTelegram, &EMS_MixingModule.hc[hc].pumpMod, EMS_OFFSET_MMStatusMessage_pump_mod); + _setValue(EMS_RxTelegram, &EMS_MixingModule.hc[hc].flowSetTemp, EMS_OFFSET_MMStatusMessage_flow_set); + + //_setValue(EMS_RxTelegram, &EMS_MixingModule.hc[hc].valveStatus, EMS_OFFSET_MMStatusMessage_valve_status); } /** @@ -1220,37 +1374,28 @@ void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { if (hc >= EMS_THERMOSTAT_MAXHC) { return; // invalid type } + EMS_Thermostat.hc[hc].active = true; - // handle single data values. data will always be at position data[0] - if (EMS_RxTelegram->data_length == 1) { - switch (EMS_RxTelegram->offset) { - case EMS_OFFSET_RCPLUSStatusMessage_curr: // setpoint target temp - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, 0); // value is * 10 - break; - case EMS_OFFSET_RCPLUSStatusMessage_setpoint: // current target temp - EMS_Thermostat.hc[hc].setpoint_roomTemp = EMS_RxTelegram->data[0]; // convert to single byte, value is * 2 - break; - case EMS_OFFSET_RCPLUSStatusMessage_currsetpoint: // current setpoint temp, e.g. Thermostat -> all, telegram: 10 00 FF 06 01 A5 22 - EMS_Thermostat.hc[hc].setpoint_roomTemp = EMS_RxTelegram->data[0]; // convert to single byte, value is * 2 - break; - case EMS_OFFSET_RCPLUSStatusMessage_mode: // thermostat mode auto/manual - // manual : 10 00 FF 0A 01 A5 02 - // auto : 10 00 FF 0A 01 A5 03 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, 0, - 0); // bit 1, mode (auto=1 or manual=0). Note this may be bit 2 - still need to validate - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].day_mode, 0, 1); // get day mode flag + // the whole telegram + // e.g. Thermostat -> all, telegram: 10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00 + // 10 00 FF 00 01 A5 80 00 01 30 28 00 30 28 01 54 03 03 01 01 54 02 A8 00 00 11 01 03 FF FF 00 + // or partial, e.g. for modes: + // manual : 10 00 FF 0A 01 A5 02 + // auto : 10 00 FF 0A 01 A5 03 - break; - } - } else if (EMS_RxTelegram->data_length > 20) { - // the whole telegram - // e.g. Thermostat -> all, telegram: 10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00 - // 10 00 FF 00 01 A5 80 00 01 30 28 00 30 28 01 54 03 03 01 01 54 02 A8 00 00 11 01 03 FF FF 00 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RCPLUSStatusMessage_curr); // value is * 10 + // current room temp + // quite often this is 0x8000 (n/a). still not sure why + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_RCPLUSStatusMessage_curr); // value is * 10 + + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode_type, EMS_OFFSET_RCPLUSStatusMessage_mode, 1); + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0) + + // setpoint is in offset 3 (EMS_OFFSET_RCPLUSStatusMessage_setpoint) and also 7 (EMS_OFFSET_RCPLUSStatusMessage_currsetpoint). + // We're sticking to 3 for now. + // also ignore if its 0 - see https://github.com/proddy/EMS-ESP/issues/256#issuecomment-585171426 + if (EMS_RxTelegram->data[EMS_OFFSET_RCPLUSStatusMessage_setpoint] != 0) { _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RCPLUSStatusMessage_setpoint); // convert to single byte, value is * 2 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].day_mode, EMS_OFFSET_RCPLUSStatusMessage_mode, 1); - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0) } } @@ -1262,20 +1407,20 @@ void _process_RCPLUSStatusMode(_EMS_RxTelegram * EMS_RxTelegram) { } /** - * FR10/FR50/FR100 Junkers - type x006F + * FR10/FR50/FR100 Junkers - type x6F * e.g. for FR10: 90 00 FF 00 00 6F 03 01 00 BE 00 BF * for FW100: 90 00 FF 00 00 6F 03 02 00 D7 00 DA F3 34 00 C4 */ void _process_JunkersStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - if (EMS_RxTelegram->offset == 0 && EMS_RxTelegram->data_length > 1) { - uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 - EMS_Thermostat.hc[hc].active = true; - - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_JunkersStatusMessage_curr); // value is * 10 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_JunkersStatusMessage_setpoint); // value is * 10 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].day_mode, EMS_OFFSET_JunkersStatusMessage_daymode); // 3 = day, 2 = night - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_JunkersStatusMessage_mode); // 1 = manual, 2 = auto + int8_t hc = _getHeatingCircuit(EMS_RxTelegram); // which HC is it, 0-3 + if (hc == -1) { + return; } + + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].curr_roomTemp, EMS_OFFSET_JunkersStatusMessage_curr); // value is * 10 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_JunkersStatusMessage_setpoint); // value is * 10 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode_type, EMS_OFFSET_JunkersStatusMessage_daymode, 0); // first bit 1=day, 0=night + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_JunkersStatusMessage_mode); // 1 = manual, 2 = auto } /** @@ -1290,25 +1435,18 @@ void _process_RCPLUSSetMessage(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 EMS_Thermostat.hc[hc].active = true; - // check for one data value - // but ignore values of 0xFF, e.g. 10 00 FF 08 01 B9 FF - if ((EMS_RxTelegram->data_length == 1) && (EMS_RxTelegram->data[0] != 0xFF)) { - // check for setpoint temps, e.g. Thermostat -> all, type 0x01B9, telegram: 10 00 FF 08 01 B9 26 - if ((EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_temp_setpoint) || (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_manual_setpoint)) { - _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, 0); // single byte conversion, value is * 2 - } else if (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_mode) { - // check for mode, eg. 10 00 FF 08 01 B9 FF - EMS_Thermostat.hc[hc].mode = (EMS_RxTelegram->data[0] == 0xFF); // Auto = xFF, Manual = x00 (auto=1 or manual=0) - } - return; // quit + // ignore single values of 0xFF, e.g. 10 00 FF 08 01 B9 FF + if ((EMS_RxTelegram->data_length == 1) && (EMS_RxTelegram->data[0] == 0xFF)) { + return; } - // check for long broadcasts - if (EMS_RxTelegram->offset == 0) { - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RCPLUSSet_mode); - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].daytemp, EMS_OFFSET_RCPLUSSet_temp_comfort2); // is * 2 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].nighttemp, EMS_OFFSET_RCPLUSSet_temp_eco); // is * 2 - } + // check for setpoint temps, e.g. Thermostat -> all, type 0x01B9, telegram: 10 00 FF 08 01 B9 26 + // NOTE when setting the room temp we pick from two values, hopefully one is correct! + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RCPLUSSet_temp_setpoint); // single byte conversion, value is * 2 + _setValue8(EMS_RxTelegram, &EMS_Thermostat.hc[hc].setpoint_roomTemp, EMS_OFFSET_RCPLUSSet_manual_setpoint); // single byte conversion, value is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RCPLUSSet_mode); // Auto = xFF, Manual = x00 eg. 10 00 FF 08 01 B9 FF + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].daytemp, EMS_OFFSET_RCPLUSSet_temp_comfort2); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].nighttemp, EMS_OFFSET_RCPLUSSet_temp_eco); // is * 2 } /** @@ -1319,6 +1457,18 @@ void _process_RC10Set(_EMS_RxTelegram * EMS_RxTelegram) { // mode not implemented yet } +/** + * type 0xAD - for reading the mode from the new RC20 thermostat (0x17) + * received only after requested + */ +void _process_RC20NSet(_EMS_RxTelegram * EMS_RxTelegram) { + uint8_t hc = EMS_THERMOSTAT_DEFAULTHC - 1; // use HC1 + EMS_Thermostat.hc[hc].active = true; + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RC20NSet_mode); // note, fixed for HC1 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].daytemp, EMS_OFFSET_RC20NSet_temp_day); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].nighttemp, EMS_OFFSET_RC20NSet_temp_night); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].heatingtype, EMS_OFFSET_RC20NSet_heatingtype); // byte 0 bit floor heating = 3 +} /** * type 0xA8 - for reading the mode from the RC20 thermostat (0x17) * received only after requested @@ -1341,32 +1491,66 @@ void _process_RC30Set(_EMS_RxTelegram * EMS_RxTelegram) { // return which heating circuit it is, 0-3 for HC1 to HC4 // based on type 0x3E (HC1), 0x48 (HC2), 0x52 (HC3), 0x5C (HC4) -uint8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram) { - uint8_t hc_num; +int8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram) { + // check to see we have an active HC. Assuming first byte must have some bit status set. + // see https://github.com/proddy/EMS-ESP/issues/238 + // and reverting on 1/2/2020 with https://github.com/proddy/EMS-ESP/issues/305#issuecomment-581006130 + /* + if (EMS_RxTelegram->data[0] == 0x00) { + return -1; + } + */ + + // ignore telegrams that have no data, or only a single byte + if (EMS_RxTelegram->data_length <= 1) { + return -1; + } + + int8_t hc; + switch (EMS_RxTelegram->type) { case EMS_TYPE_RC35StatusMessage_HC1: case EMS_TYPE_RC35Set_HC1: - default: - hc_num = 1; // also default + case EMS_TYPE_JunkersStatusMessage_HC1: + case EMS_TYPE_JunkersSetMessage1_HC1: + case EMS_TYPE_JunkersSetMessage2_HC1: + hc = 0; break; + case EMS_TYPE_RC35StatusMessage_HC2: case EMS_TYPE_RC35Set_HC2: - hc_num = 2; + case EMS_TYPE_JunkersStatusMessage_HC2: + case EMS_TYPE_JunkersSetMessage1_HC2: + case EMS_TYPE_JunkersSetMessage2_HC2: + hc = 1; break; + case EMS_TYPE_RC35StatusMessage_HC3: case EMS_TYPE_RC35Set_HC3: - hc_num = 3; + case EMS_TYPE_JunkersStatusMessage_HC3: + case EMS_TYPE_JunkersSetMessage1_HC3: + case EMS_TYPE_JunkersSetMessage2_HC3: + hc = 2; break; + case EMS_TYPE_RC35StatusMessage_HC4: case EMS_TYPE_RC35Set_HC4: - hc_num = 4; + case EMS_TYPE_JunkersStatusMessage_HC4: + case EMS_TYPE_JunkersSetMessage1_HC4: + case EMS_TYPE_JunkersSetMessage2_HC4: + hc = 3; + break; + + default: + hc = -1; // not a valid HC break; } - hc_num--; - EMS_Thermostat.hc[hc_num].active = true; + if (hc != -1) { + EMS_Thermostat.hc[hc].active = true; + } - return (hc_num); + return (hc); } /** @@ -1384,13 +1568,16 @@ void _process_RC35Set(_EMS_RxTelegram * EMS_RxTelegram) { return; } - uint8_t hc_num = _getHeatingCircuit(EMS_RxTelegram); // which HC is it? + int8_t hc = _getHeatingCircuit(EMS_RxTelegram); // which HC is it, 0-3 + if (hc == -1) { + return; + } - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].mode, EMS_OFFSET_RC35Set_mode); // night, day, auto - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].daytemp, EMS_OFFSET_RC35Set_temp_day); // is * 2 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].nighttemp, EMS_OFFSET_RC35Set_temp_night); // is * 2 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].holidaytemp, EMS_OFFSET_RC35Set_temp_holiday); // is * 2 - _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc_num].heatingtype, EMS_OFFSET_RC35Set_heatingtype); // byte 0 bit floor heating = 3 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].mode, EMS_OFFSET_RC35Set_mode); // night, day, auto + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].daytemp, EMS_OFFSET_RC35Set_temp_day); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].nighttemp, EMS_OFFSET_RC35Set_temp_night); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].holidaytemp, EMS_OFFSET_RC35Set_temp_holiday); // is * 2 + _setValue(EMS_RxTelegram, &EMS_Thermostat.hc[hc].heatingtype, EMS_OFFSET_RC35Set_heatingtype); // byte 0 bit floor heating = 3 } /** @@ -1411,43 +1598,39 @@ void _process_SM10Monitor(_EMS_RxTelegram * EMS_RxTelegram) { } /* - * SM100Monitor - type 0x0262 EMS+ + * SM100Monitor - type 0x0262 EMS+ - for SM100 and SM200 + * e.g. B0 0B FF 00 02 62 00 44 02 7A 80 00 80 00 80 00 80 00 80 00 80 00 00 7C 80 00 80 00 80 00 80 * e.g, 30 00 FF 00 02 62 01 AC * 30 00 FF 18 02 62 80 00 * 30 00 FF 00 02 62 01 A1 - for bottom temps + * bytes 0+1 = TS1 Temperature sensor for collector + * bytes 2+3 = TS2 Temperature sensor bottom cylinder 1 + * bytes 16+17 = TS5 Temperature sensor bottom cylinder 2 */ void _process_SM100Monitor(_EMS_RxTelegram * EMS_RxTelegram) { - // only process the complete telegram, not partial - if (EMS_RxTelegram->offset) { - return; - } - _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 0); // is *10 _setValue(EMS_RxTelegram, &EMS_SolarModule.bottomTemp, 2); // is *10 + _setValue(EMS_RxTelegram, &EMS_SolarModule.bottomTemp2, 16); // is *10 } /* - * SM100Status - type 0x0264 EMS+ for pump modulation + * SM100Status - type 0x0264 EMS+ for pump modulation - for SM100 and SM200 * e.g. 30 00 FF 09 02 64 64 = 100% * 30 00 FF 09 02 64 1E = 30% */ void _process_SM100Status(_EMS_RxTelegram * EMS_RxTelegram) { - if (EMS_RxTelegram->offset == 0) { - _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpModulation, 9); // check for complete telegram - } else if (EMS_RxTelegram->offset == 0x09) { - _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpModulation, 0); // data at offset 09 - } + _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpModulation, 9); // check for complete telegram } /* - * SM100Status2 - type 0x026A EMS+ for pump on/off at offset 0x0A + * SM100Status2 - type 0x026A EMS+ for pump on/off at offset 0x0A - for SM100 and SM200 + * e.g. B0 00 FF 00 02 6A 03 03 03 03 01 03 03 03 03 03 01 03 + * byte 4 = VS2 3-way valve for cylinder 2 : test=01, on=04 and off=03 + * byte 10 = PS1 Solar circuit pump for collector array 1: test=01, on=04 and off=03 */ void _process_SM100Status2(_EMS_RxTelegram * EMS_RxTelegram) { - if (EMS_RxTelegram->offset == 0) { - _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 10, 2); // 03=off 04=on - } else if (EMS_RxTelegram->offset == 0x0A) { - _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 0, 2); // 03=off 04=on at offset 0A - } + _setValue(EMS_RxTelegram, &EMS_SolarModule.valveStatus, 4, 2); // on if bit 2 set + _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 10, 2); // on if bit 2 set } /* @@ -1455,9 +1638,9 @@ 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) { - _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyLastHour, 2); // last hour / 10 in Wh - _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyToday, 6); // todays in Wh - _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyTotal, 10); // total / 10 in kWh + _setValue32(EMS_RxTelegram, &EMS_SolarModule.EnergyLastHour, 0); // last hour / 10 in Wh + _setValue32(EMS_RxTelegram, &EMS_SolarModule.EnergyToday, 4); // todays in Wh + _setValue32(EMS_RxTelegram, &EMS_SolarModule.EnergyTotal, 8); // total / 10 in kWh } /* @@ -1479,18 +1662,11 @@ void _process_HPMonitor2(_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 */ void _process_ISM1StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { - if (EMS_RxTelegram->offset == 0) { - _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 4); // Collector Temperature - _setValue(EMS_RxTelegram, &EMS_SolarModule.bottomTemp, 6); // Temperature Bottom of Solar Boiler - _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyLastHour, 2); // Solar Energy produced in last hour - is * 10 and handled in ems-esp.cpp - _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 8, 0); // Solar pump on (1) or off (0) - _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpWorkMin, 10); - } - - if (EMS_RxTelegram->offset == 4) { - // e.g. B0 00 FF 04 00 03 02 E5 - _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 0); // Collector Temperature - } + _setValue(EMS_RxTelegram, &EMS_SolarModule.collectorTemp, 4); // Collector Temperature + _setValue(EMS_RxTelegram, &EMS_SolarModule.bottomTemp, 6); // Temperature Bottom of Solar Boiler + _setValue(EMS_RxTelegram, &EMS_SolarModule.EnergyLastHour, 2); // Solar Energy produced in last hour - is * 10 and handled in ems-esp.cpp + _setValue(EMS_RxTelegram, &EMS_SolarModule.pump, 8, 0); // Solar pump on (1) or off (0) + _setValue(EMS_RxTelegram, &EMS_SolarModule.pumpWorkMin, 10); } @@ -1498,11 +1674,12 @@ void _process_ISM1StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) { * 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 = EMS_RxTelegram->data[0]; - } + // e.g. 90 30 FF 06 00 01 50 + // only trigger if at offset 6 + _setValue(EMS_RxTelegram, &EMS_SolarModule.setpoint_maxBottomTemp, 6); + + // Note: we may need to convert this to a single byte like + // EMS_SolarModule.setpoint_maxBottomTemp = EMS_RxTelegram->data[0]; } /** @@ -1523,7 +1700,7 @@ void _process_SetPoints(_EMS_RxTelegram * EMS_RxTelegram) { myDebug_P(PSTR(" Boiler flow temp %s C, Warm Water power %d %"), s, ww_power); */ - myDebug_P(PSTR(" Boiler flow temperature is %d C"), setpoint); + myDebug_P(PSTR("Boiler flow temperature is %d C"), setpoint); } } } @@ -1556,26 +1733,15 @@ void _process_RCTime(_EMS_RxTelegram * EMS_RxTelegram) { strlcpy(EMS_Thermostat.datetime, time_sp, sizeof(time_sp)); // store } -/* - * Clear devices list - */ -void ems_clearDeviceList() { - Devices.clear(); - - for (uint8_t i = 0; i < EMS_SYS_DEVICEMAP_LENGTH; i++) { - EMS_Sys_Status.emsDeviceMap[i] = 0x00; - } -} - /* * add an EMS device to our list of detected devices if its unique * returns true if already in list */ -bool _addDevice(_EMS_DEVICE_TYPE device_type, uint8_t product_id, uint8_t device_id, const char * device_desc_p, const char * version) { +bool _addDevice(_EMS_DEVICE_TYPE device_type, uint8_t product_id, uint8_t device_id, const char * device_desc_p, const char * version, uint8_t brand) { _Detected_Device device; // check for duplicates - // a combi of product_id and device_id make it unique + // a pair of product_id and device_id together make it unique for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { if (((it)->product_id == product_id) && ((it)->device_id == device_id)) { return (true); // it already exists in the list, don't add @@ -1594,12 +1760,24 @@ bool _addDevice(_EMS_DEVICE_TYPE device_type, uint8_t product_id, uint8_t device char line[500]; strlcpy(line, "New EMS device recognized as a ", sizeof(line)); + if (brand == 1) { + strlcat(line, "Bosch ", sizeof(line)); + } else if (brand == 2) { + strlcat(line, "Junkers ", sizeof(line)); + } else if (brand == 3) { + strlcat(line, "Buderus ", sizeof(line)); + } else if (brand == 4) { + strlcat(line, "Nefit ", sizeof(line)); + } else if (brand == 5) { + strlcat(line, "Sieger ", sizeof(line)); + } else if (brand == 11) { + strlcat(line, "Worcester ", sizeof(line)); + } + // get type as a string char type_s[50]; - if (ems_getDeviceTypeDescription(device_id, type_s)) { + if (ems_getDeviceTypeName(device_type, type_s)) { strlcat(line, type_s, sizeof(line)); - } else { - strlcat(line, "?", sizeof(line)); } char tmp[6] = {0}; // for formatting numbers @@ -1609,11 +1787,11 @@ bool _addDevice(_EMS_DEVICE_TYPE device_type, uint8_t product_id, uint8_t device strlcat(line, device_desc_p, sizeof(line)); } - strlcat(line, " (DeviceID:0x", sizeof(line)); + strlcat(line, " (DeviceID: 0x", sizeof(line)); strlcat(line, _hextoa(device_id, tmp), sizeof(line)); - strlcat(line, " ProductID:", sizeof(line)); + strlcat(line, ", ProductID: ", sizeof(line)); strlcat(line, itoa(product_id, tmp, 10), sizeof(line)); - strlcat(line, " Version:", sizeof(line)); + strlcat(line, ", Version: ", sizeof(line)); strlcat(line, version, sizeof(line)); strlcat(line, ")", sizeof(line)); @@ -1624,38 +1802,44 @@ bool _addDevice(_EMS_DEVICE_TYPE device_type, uint8_t product_id, uint8_t device /** * type 0x07 - shows us the connected EMS devices - * e.g. 08 00 07 00 0B 80 00 00 00 00 00 00 00 00 00 00 00 (CRC=47) #data=13 - * Junkers is 15 (I think) + * e.g. 08 00 07 00 0B 80 00 00 00 00 00 00 00 00 00 00 00 + * Junkers has 15 bytes of data + * each byte is a bitmask for which devices are active + * byte 1 = range 0x08 - 0x0F, byte 2=0x10 - 0x17 etc... */ void _process_UBADevices(_EMS_RxTelegram * EMS_RxTelegram) { - if (EMS_RxTelegram->data_length > EMS_SYS_DEVICEMAP_LENGTH) { - return; // should be 13 or 15 bytes long + // exit it length is incorrect (13 or 15 bytes long) + // or it wasn't specifically for us + // or we can't write to the EMS bus yet + if ((EMS_RxTelegram->data_length > EMS_SYS_DEVICEMAP_LENGTH) || (EMS_RxTelegram->dest != EMS_Sys_Status.emsbusid) || (ems_getTxDisabled())) { + return; } + // for each byte, check the bits and determine the device_id for (uint8_t data_byte = 0; data_byte < EMS_RxTelegram->data_length; data_byte++) { - uint8_t byte = EMS_RxTelegram->data[data_byte]; - uint8_t saved_byte = EMS_Sys_Status.emsDeviceMap[data_byte]; - - // see if this matches what we already have stored - if (byte != saved_byte) { - // we have something new - EMS_Sys_Status.emsDeviceMap[data_byte] = byte; // save new value - // go through all bits - // myDebug("Byte #%d 0x%02X", data_byte, byte); // for debugging - if (byte) { - for (uint8_t bit = 0; bit < 8; bit++) { - if ((byte & 0x01) && ((saved_byte & 0x01) == 0)) { - uint8_t device_id = ((data_byte + 1) * 8) + bit; - if (device_id != EMS_ID_ME) { - // myDebug("[EMS] Detected new EMS Device with ID 0x%02X", device_id); - if (!ems_getTxDisabled()) { - ems_doReadCommand(EMS_TYPE_Version, device_id); // get version, but ignore ourselves + uint8_t byte = EMS_RxTelegram->data[data_byte]; + if (byte) { + for (uint8_t bit = 0; bit < 8; bit++) { + if (byte & 0x01) { + uint8_t device_id = ((data_byte + 1) * 8) + bit; + // see if we already have this device in our list + // ignore ourselves, we're not an EMS device + if (device_id != EMS_Sys_Status.emsbusid) { + bool exists = false; + if (!Devices.empty()) { + for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + if (it->device_id == device_id) { + exists = true; + } } } + if (!exists) { + myDebug_P(PSTR("[EMS] Detected new EMS Device with ID 0x%02X. Fetching version information..."), device_id); + ems_doReadCommand(EMS_TYPE_Version, device_id); // get version, but ignore ourselves + } } - byte = byte >> 1; - saved_byte = saved_byte >> 1; } + byte = byte >> 1; // advance 1 bit } } } @@ -1671,10 +1855,9 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { return; } - uint8_t offset = 0; - // check for 2nd subscriber // e.g. 18 0B 02 00 00 00 00 5E 02 01 + uint8_t offset = 0; if (EMS_RxTelegram->data[0] == 0x00) { // see if we have a 2nd subscriber if (EMS_RxTelegram->data[3] != 0x00) { @@ -1684,7 +1867,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { } } - uint8_t device_id = EMS_RxTelegram->src; // device ID + uint8_t device_id = EMS_RxTelegram->src; // device ID + uint8_t product_id = EMS_RxTelegram->data[offset]; // product ID // get version as XX.XX char version[10] = {0}; @@ -1693,49 +1877,89 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { strlcat(version, ".", sizeof(version)); strlcat(version, _smallitoa(EMS_RxTelegram->data[offset + 2], buf), sizeof(version)); - // scan through known devices matching the productid - uint8_t product_id = EMS_RxTelegram->data[offset]; - uint8_t i = 0; - bool typeFound = false; + // some devices store the protocol type (HT3, Buderus) in the last byte + // 0=unknown, 1=bosch, 2=junkers, 3=buderus, 4=nefit, 5=sieger, 11=worcester + uint8_t brand; + if (EMS_RxTelegram->data_length >= 10) { + brand = EMS_RxTelegram->data[9]; + } else { + brand = 0; // unknown + } + +#ifdef EMSESP_SIMULATE + // override to emulate other thermostats - FR100 + if (device_id == 0x17) { + brand = 2; + device_id = 0x10; + //product_id = 107; // FR100 + product_id = 192; // FW120 + } +#endif + + // first scan through matching boilers, as these are unique to DeviceID 0x08 + uint8_t i = 0; while (i < _EMS_Devices_max) { - if (EMS_Devices[i].product_id == product_id) { - typeFound = true; // we have a matching product id. i is the index. + if ((EMS_Devices[i].product_id == product_id) && (EMS_Devices[i].type == EMS_DEVICE_TYPE_BOILER) && (device_id == EMS_ID_BOILER)) { + // we have a matching boiler, add it then quit + EMS_Boiler.device_id = EMS_ID_BOILER; + EMS_Boiler.device_flags = EMS_DEVICE_FLAG_NONE; + EMS_Boiler.product_id = product_id; + EMS_Boiler.device_desc_p = EMS_Devices[i].device_desc; + strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); + + // set brand of boiler + EMS_Boiler.brand = brand; + + _addDevice(EMS_DEVICE_TYPE_BOILER, product_id, EMS_ID_BOILER, EMS_Devices[i].device_desc, version, brand); + ems_getBoilerValues(); // get Boiler values that we would usually have to wait for + return; // quit + } + i++; + } + + // not a boiler, continue by matching the product_id + i = 0; + uint8_t found_index = 0; + bool typeFound = false; + while (i < _EMS_Devices_max) { + if ((EMS_Devices[i].product_id == product_id) && (EMS_Devices[i].type != EMS_DEVICE_TYPE_BOILER)) { + // we have a matching product id + typeFound = true; + found_index = i; break; } i++; } - // if not found, just add it + // if not found, just add it as an unknown device and exit if (!typeFound) { - (void)_addDevice(EMS_DEVICE_TYPE_UNKNOWN, product_id, device_id, nullptr, version); + (void)_addDevice(EMS_DEVICE_TYPE_UNKNOWN, product_id, device_id, nullptr, version, 0); return; } - const char * device_desc_p = (EMS_Devices[i].device_desc); // pointer to the full description of the device - _EMS_DEVICE_TYPE type = EMS_Devices[i].type; // type + const char * device_desc_p = (EMS_Devices[found_index].device_desc); // pointer to the full description of the device + _EMS_DEVICE_TYPE type = EMS_Devices[found_index].type; // device type - // we recognized it, see if we already have it in our recognized list - if (_addDevice(type, product_id, device_id, device_desc_p, version)) { - return; // already in list + // we recognized it, add it to list + if (_addDevice(type, product_id, device_id, device_desc_p, version, brand)) { + return; // already in list, don't bother initializing it } - uint8_t flags = EMS_Devices[i].flags; // its a new entry, set the specifics + uint8_t flags = EMS_Devices[found_index].flags; // it's a new entry, get the specifics - if (type == EMS_DEVICE_TYPE_BOILER) { - EMS_Boiler.device_id = device_id; - EMS_Boiler.product_id = product_id; - EMS_Boiler.device_flags = flags; - EMS_Boiler.device_desc_p = device_desc_p; - strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); - ems_getBoilerValues(); // get Boiler values that we would usually have to wait for - } else if (type == EMS_DEVICE_TYPE_THERMOSTAT) { - EMS_Thermostat.device_id = device_id; - EMS_Thermostat.device_flags = (flags & 0x7F); // remove 7th bit - EMS_Thermostat.write_supported = (flags & EMS_DEVICE_FLAG_NO_WRITE) == 0; - EMS_Thermostat.product_id = product_id; - EMS_Thermostat.device_desc_p = device_desc_p; - strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); - ems_getThermostatValues(); // get Thermostat values + if (type == EMS_DEVICE_TYPE_THERMOSTAT) { + // we can only support a single thermostat currently, so check which product_id we may have chosen + // to be the master - see https://github.com/proddy/EMS-ESP/issues/238 + if (((EMS_Sys_Status.emsMasterThermostat == 0) && (EMS_Thermostat.device_id == EMS_ID_NONE)) || (EMS_Sys_Status.emsMasterThermostat == product_id)) { + EMS_Thermostat.device_id = device_id; + EMS_Thermostat.device_flags = (flags & 0x7F); // remove 7th bit + EMS_Thermostat.write_supported = (flags & EMS_DEVICE_FLAG_NO_WRITE) == 0; + EMS_Thermostat.product_id = product_id; + EMS_Thermostat.device_desc_p = device_desc_p; + strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); + ems_getThermostatValues(); // get Thermostat values + ems_getSettingsValues(); // get Settings from Thermostat + } } else if (type == EMS_DEVICE_TYPE_SOLAR) { EMS_SolarModule.device_id = device_id; EMS_SolarModule.product_id = product_id; @@ -1750,12 +1974,12 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { EMS_HeatPump.device_desc_p = device_desc_p; strlcpy(EMS_HeatPump.version, version, sizeof(EMS_HeatPump.version)); } else if (type == EMS_DEVICE_TYPE_MIXING) { - EMS_Mixing.device_id = device_id; - EMS_Mixing.product_id = product_id; - EMS_Mixing.device_desc_p = device_desc_p; - EMS_Mixing.device_flags = flags; - EMS_Mixing.detected = true; - ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_HC1, device_id); // fetch MM values + EMS_MixingModule.device_id = device_id; + EMS_MixingModule.product_id = product_id; + EMS_MixingModule.device_desc_p = device_desc_p; + EMS_MixingModule.device_flags = flags; + strlcpy(EMS_MixingModule.version, version, sizeof(EMS_MixingModule.version)); + ems_getMixingModuleValues(); // fetch Mixing Module values } } @@ -1763,12 +1987,12 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) { * Figure out the boiler and thermostat types */ void ems_discoverModels() { - //myDebug_P(PSTR("Starting auto discover of EMS devices...")); + // ask Boiler for it's known devices ems_doReadCommand(EMS_TYPE_UBADevices, EMS_ID_BOILER); } /** - * Print the Tx queue - for debugging + * Print the Tx queue */ void ems_printTxQueue() { _EMS_TxTelegram EMS_TxTelegram; @@ -1840,6 +2064,10 @@ void ems_getThermostatValues() { ems_doReadCommand(EMS_TYPE_RC20StatusMessage, device_id); // to get the temps ems_doReadCommand(EMS_TYPE_RC20Set, device_id); // to get the mode break; + case EMS_DEVICE_FLAG_RC20N: + ems_doReadCommand(EMS_TYPE_RC20NStatusMessage, device_id); // to get the temps + ems_doReadCommand(EMS_TYPE_RC20NSet, device_id); // to get the mode + break; case EMS_DEVICE_FLAG_RC30: ems_doReadCommand(EMS_TYPE_RC30StatusMessage, device_id); // to get the temps ems_doReadCommand(EMS_TYPE_RC30Set, device_id); // to get the mode @@ -1848,6 +2076,7 @@ void ems_getThermostatValues() { ems_doReadCommand(EMS_TYPE_EasyStatusMessage, device_id); break; case EMS_DEVICE_FLAG_RC35: + case EMS_DEVICE_FLAG_RC30N: for (uint8_t hc_num = 1; hc_num <= EMS_THERMOSTAT_MAXHC; hc_num++) { if (hc_num == 1) { statusMsg = EMS_TYPE_RC35StatusMessage_HC1; @@ -1862,11 +2091,13 @@ void ems_getThermostatValues() { statusMsg = EMS_TYPE_RC35StatusMessage_HC4; opMode = EMS_TYPE_RC35Set_HC4; } - ems_doReadCommand(statusMsg, device_id); // to get the temps - ems_doReadCommand(opMode, device_id); // to get the mode + ems_doReadCommand(statusMsg, device_id); // to get the temps + ems_doReadCommand(opMode, device_id); // to get the mode + ems_doReadCommand(opMode, device_id, 27); // to get the mode } break; case EMS_DEVICE_FLAG_RC300: + case EMS_DEVICE_FLAG_RC100: ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage_HC1, device_id); ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage_HC2, device_id); ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage_HC3, device_id); @@ -1882,61 +2113,129 @@ void ems_getThermostatValues() { * Generic function to return various settings from the thermostat */ void ems_getBoilerValues() { - ems_doReadCommand(EMS_TYPE_UBAMonitorFast, EMS_Boiler.device_id); // get boiler stats, instead of waiting 10secs for the broadcast - ems_doReadCommand(EMS_TYPE_UBAMonitorSlow, EMS_Boiler.device_id); // get more boiler stats, instead of waiting 60secs for the broadcast + ems_doReadCommand(EMS_TYPE_UBAMonitorFast, EMS_Boiler.device_id); // get boiler data, instead of waiting 10secs for the broadcast + ems_doReadCommand(EMS_TYPE_UBAMonitorSlow, EMS_Boiler.device_id); // get more boiler data, instead of waiting 60secs for the broadcast ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_Boiler.device_id); // get Warm Water values ems_doReadCommand(EMS_TYPE_UBAParametersMessage, EMS_Boiler.device_id); // get MC10 boiler values ems_doReadCommand(EMS_TYPE_UBATotalUptimeMessage, EMS_Boiler.device_id); // get uptime from boiler } +void ems_getSettingsValues() { + if (!ems_getThermostatEnabled()) { + return; + } + + uint8_t device_flags = EMS_Thermostat.device_flags; + uint8_t device_id = EMS_Thermostat.device_id; + + // for the moment, only validated on RC30 + switch (device_flags) { + case EMS_DEVICE_FLAG_RC30N: + ems_doReadCommand(EMS_TYPE_IBASettingsMessage, device_id); + break; + default: + break; + } +} + /* - * Get other values from EMS devices + * Get solar values from EMS devices */ void ems_getSolarModuleValues() { if (ems_getSolarModuleEnabled()) { if (EMS_SolarModule.device_flags == EMS_DEVICE_FLAG_SM10) { - ems_doReadCommand(EMS_TYPE_SM10Monitor, EMS_ID_SM); // fetch all from SM10Monitor + ems_doReadCommand(EMS_TYPE_SM10Monitor, EMS_SolarModule.device_id); // fetch all from SM10Monitor } else if (EMS_SolarModule.device_flags == EMS_DEVICE_FLAG_SM100) { - ems_doReadCommand(EMS_TYPE_SM100Monitor, EMS_ID_SM); // fetch all from SM100Monitor + ems_doReadCommand(EMS_TYPE_SM100Monitor, EMS_SolarModule.device_id); // fetch all from SM100Monitor (also for SM200) } } } +/* + * Get mixing module values from EMS devices + */ +void ems_getMixingModuleValues() { + if (ems_getMixingModuleEnabled()) { + if (EMS_MixingModule.device_flags == EMS_DEVICE_FLAG_MMPLUS) { + ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_HC1, EMS_MixingModule.device_id); + ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_HC2, EMS_MixingModule.device_id); + ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_HC3, EMS_MixingModule.device_id); + ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_HC4, EMS_MixingModule.device_id); + ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_WWC1, EMS_MixingModule.device_id); + ems_doReadCommand(EMS_TYPE_MMPLUSStatusMessage_WWC2, EMS_MixingModule.device_id); + } else if (EMS_MixingModule.device_flags == EMS_DEVICE_FLAG_MM10) { + ems_doReadCommand(EMS_TYPE_MMStatusMessage, EMS_MixingModule.device_id); + } + } +} + +// takes a device type (e.g. EMS_DEVICE_TYPE_MIXING) and stores the english name in the given buffer +// returns buffer or "unknown" +char * ems_getDeviceTypeName(_EMS_DEVICE_TYPE device_type, char * buffer) { + uint8_t i = 0; + bool typeFound = false; + // scan through known ID types + while (i < _EMS_Devices_Types_max) { + if (EMS_Devices_Types[i].device_type == device_type) { + typeFound = true; // we have a match + break; + } + i++; + } + + if (!typeFound) { + i = 0; // this will point to "Unknown" in the lookup + } + + strlcpy(buffer, EMS_Devices_Types[i].device_type_string, 30); + + return buffer; +} + /** * takes a device_id and tries to find the corresponding type name (e.g. Boiler) * If it can't find it, it will use the hex value and function returns false */ bool ems_getDeviceTypeDescription(uint8_t device_id, char * buffer) { - uint8_t i = 0; - bool typeFound = false; + _EMS_DEVICE_TYPE device_type = EMS_DEVICE_TYPE_UNKNOWN; - // scan through known ID types - while (i < _EMS_Devices_Types_max) { - if (EMS_Devices_Types[i].device_id == device_id) { - typeFound = true; // we have a match - break; - } - i++; - } - - if (typeFound) { - strlcpy(buffer, EMS_Devices_Types[i].device_type_string, 30); - return true; + // check for the fixed device IDs we already know about, like 0x00 for broadcast, 0x0B for me, 0x08 for Boiler + if (device_id == EMS_ID_BOILER) { + device_type = EMS_DEVICE_TYPE_BOILER; + } else if (device_id == EMS_Sys_Status.emsbusid) { + device_type = EMS_DEVICE_TYPE_SERVICEKEY; + } else if (device_id == EMS_ID_NONE) { + device_type = EMS_DEVICE_TYPE_NONE; } else { - // print as hex value - char hexbuffer[16] = {0}; - strlcpy(buffer, "0x", 30); - strlcat(buffer, _hextoa(device_id, hexbuffer), 30); - return false; + // see if its a device we already know about (via earlier detection) + if (!Devices.empty()) { + for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { + if (it->device_id == device_id) { + device_type = it->device_type; + break; + } + } + } } -} + // if its not unknown, fetch the real name of the type + if (device_type != EMS_DEVICE_TYPE_UNKNOWN) { + ems_getDeviceTypeName(device_type, buffer); + return true; + } + + // we didn't find anything. Use the hex value of the device ID + char hexbuffer[16] = {0}; + strlcpy(buffer, "0x", 30); + strlcat(buffer, _hextoa(device_id, hexbuffer), 30); + return false; +} /** * returns current device details as a string for known thermostat,boiler,solar and heatpump */ char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, bool name_only) { - const uint8_t size = 128; + const uint8_t size = 200; bool enabled = false; uint8_t device_id; uint8_t product_id; @@ -1967,6 +2266,12 @@ char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, boo product_id = EMS_HeatPump.product_id; device_desc_p = EMS_HeatPump.device_desc_p; version = EMS_HeatPump.version; + } else if (device_type == EMS_DEVICE_TYPE_MIXING) { + enabled = ems_getMixingModuleEnabled(); + device_id = EMS_MixingModule.device_id; + product_id = EMS_MixingModule.product_id; + device_desc_p = EMS_MixingModule.device_desc_p; + version = EMS_MixingModule.version; } if (!enabled) { @@ -1986,109 +2291,65 @@ char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, boo return buffer; // only interested in the model name } - strlcat(buffer, " (DeviceID:0x", size); + strlcat(buffer, " (DeviceID: 0x", size); char tmp[6] = {0}; strlcat(buffer, _hextoa(device_id, tmp), size); - strlcat(buffer, " ProductID:", size); + strlcat(buffer, ", ProductID: ", size); strlcat(buffer, itoa(product_id, tmp, 10), size); - strlcat(buffer, " Version:", size); + strlcat(buffer, ", Version: ", size); strlcat(buffer, version, size); strlcat(buffer, ")", size); return buffer; } -/** - * Find the versions of our connected devices - */ -void ems_scanDevices() { - myDebug_P(PSTR("Started scanning the EMS bus for known devices")); - - std::list Device_Ids; // create a new list - - Device_Ids.push_back(EMS_ID_BOILER); // UBAMaster/Boilers - 0x08 - Device_Ids.push_back(EMS_ID_HP); // HeatPump - 0x38 - Device_Ids.push_back(EMS_ID_SM); // Solar Module - 0x30 - Device_Ids.push_back(0x09); // Controllers - 0x09 - Device_Ids.push_back(0x02); // Connect - 0x02 - Device_Ids.push_back(0x48); // Gateway - 0x48 - Device_Ids.push_back(0x20); // Mixing Devices - 0x20, 0x21 - Device_Ids.push_back(0x21); // Mixing Devices - 0x20, 0x21 - Device_Ids.push_back(0x10); // Thermostats - 0x10, 0x17, 0x18 - Device_Ids.push_back(0x17); // Thermostats - 0x10, 0x17, 0x18 - Device_Ids.push_back(0x18); // Thermostats - 0x10, 0x17, 0x18 - - // remove duplicates and reserved IDs (like our own device) - Device_Ids.sort(); - // Device_Ids.unique(); - - // send the read command with Version command - for (uint8_t device_id : Device_Ids) { - ems_doReadCommand(EMS_TYPE_Version, device_id); - } -} - /** * print out contents of the device list that was captured */ void ems_printDevices() { - char s[100]; - char buffer[16] = {0}; - - strlcpy(s, "These device IDs are on the EMS Bus:", sizeof(s)); - strlcat(s, COLOR_BOLD_ON, sizeof(s)); - - for (uint8_t data_byte = 0; data_byte < EMS_SYS_DEVICEMAP_LENGTH; data_byte++) { - uint8_t byte = EMS_Sys_Status.emsDeviceMap[data_byte]; - if (byte) { - // go through all bits - for (uint8_t bit = 0; bit < 8; bit++) { - if (byte & 0x01) { - uint8_t device_id = ((data_byte + 1) * 8) + bit; - if (device_id != EMS_ID_ME) { - strlcat(s, " 0x", sizeof(s)); - strlcat(s, _hextoa(device_id, buffer), sizeof(s)); - } - } - byte = byte >> 1; - } - } - } - - strlcat(s, COLOR_BOLD_OFF, sizeof(s)); - myDebug_P(PSTR("")); // newline - myDebug(s); - // print out the ones we recognized if (!Devices.empty()) { bool have_unknowns = false; char device_string[100]; - myDebug_P(PSTR("and %d were recognized by EMS-ESP as:"), Devices.size()); + char device_type[30]; + myDebug_P(PSTR("These %d were recognized by EMS-ESP:"), Devices.size()); for (std::list<_Detected_Device>::iterator it = Devices.begin(); it != Devices.end(); ++it) { - if ((it)->known) { - strlcpy(device_string, (it)->device_desc_p, sizeof(device_string)); + ems_getDeviceTypeName(it->device_type, device_type); // get type string, e.g. "Boiler" + if (it->known) { + strlcpy(device_string, it->device_desc_p, sizeof(device_string)); } else { strlcpy(device_string, EMS_MODELTYPE_UNKNOWN_STRING, sizeof(device_string)); // Unknown have_unknowns = true; } - myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d Version:%s)"), - COLOR_BOLD_ON, - device_string, - COLOR_BOLD_OFF, - (it)->device_id, - (it)->product_id, - (it)->version); - } + if ((it->device_type == EMS_DEVICE_TYPE_THERMOSTAT) && (EMS_Sys_Status.emsMasterThermostat == it->product_id)) { + myDebug_P(PSTR(" %s: %s%s%s (DeviceID: 0x%02X, ProductID: %d, Version: %s) [master]"), + device_type, + COLOR_BOLD_ON, + device_string, + COLOR_BOLD_OFF, + it->device_id, + it->product_id, + it->version); - myDebug_P(PSTR("")); // newline + } else { + myDebug_P(PSTR(" %s: %s%s%s (DeviceID: 0x%02X, ProductID: %d, Version: %s)"), + device_type, + COLOR_BOLD_ON, + device_string, + COLOR_BOLD_OFF, + it->device_id, + it->product_id, + it->version); + } + } if (have_unknowns) { - myDebug_P( - PSTR("You have a device is that is not yet known by EMS-ESP. Please report this as a GitHub issue so we can expand the EMS device library.")); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("One or more devices are not recognized by EMS-ESP. Please report this back in GitHub.")); } } else { - myDebug_P(PSTR("No were devices recognized. This may be because Tx is disabled or failing.")); + myDebug_P(PSTR("No devices were recognized. This may be because Tx is disabled or failing.")); } myDebug_P(PSTR("")); // newline @@ -2119,7 +2380,7 @@ void ems_sendRawTelegram(char * telegram) { strlcpy(value, p, sizeof(value)); EMS_TxTelegram.data[0] = (uint8_t)strtol(value, 0, 16); } - // and interate until end + // and iterate until end while (p != 0) { if ((p = strtok(nullptr, " ,"))) { strlcpy(value, p, sizeof(value)); @@ -2147,12 +2408,17 @@ void ems_sendRawTelegram(char * telegram) { EMS_TxQueue.push(EMS_TxTelegram); } +// wrapper for setting thermostat temp, taking mode as a string argument +void ems_setThermostatTemp(float temperature, uint8_t hc, const char * mode_s) { + ems_setThermostatTemp(temperature, hc, ems_getThermostatMode(mode_s)); +} + /** * Set the temperature of the thermostat * hc_num is 1 to 4 - * temptype 0 = normal, 1=night temp, 2=day temp, 3=holiday temp + * temptype 0=normal, 1=night temp, 2=day temp, 3=holiday temp */ -void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) { +void ems_setThermostatTemp(float temperature, uint8_t hc, _EMS_THERMOSTAT_MODE temptype) { if (!ems_getThermostatEnabled()) { myDebug_P(PSTR("Thermostat not online.")); return; @@ -2163,7 +2429,7 @@ void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) return; } - if (hc_num < 1 || hc_num > EMS_THERMOSTAT_MAXHC) { + if (hc < 1 || hc > EMS_THERMOSTAT_MAXHC) { myDebug_P(PSTR("Invalid HC number")); return; } @@ -2172,97 +2438,187 @@ void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter - uint8_t model = ems_getThermostatModel(); + uint8_t model = ems_getThermostatFlags(); uint8_t device_id = EMS_Thermostat.device_id; EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; EMS_TxTelegram.dest = device_id; + // get mode as a string + char mode_str[10]; + ems_getThermostatModeString(temptype, mode_str); char s[10] = {0}; - myDebug_P(PSTR("Setting new thermostat temperature to %s for heating circuit %d type %d (0=auto,1=night,2=day,3=holiday)"), - _float_to_char(s, temperature), - hc_num, - temptype); + myDebug_P(PSTR("Setting thermostat temperature to %s for heating circuit %d, mode %s"), _float_to_char(s, temperature), hc, mode_str); - if (model == EMS_DEVICE_FLAG_RC20) { - EMS_TxTelegram.type = EMS_TYPE_RC20Set; - EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_temp; - EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage; - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + if (model == EMS_DEVICE_FLAG_RC10) { + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; + EMS_TxTelegram.type = EMS_TYPE_RC10Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC10Set_temp; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC10StatusMessage; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + } - } else if (model == EMS_DEVICE_FLAG_RC10) { - EMS_TxTelegram.type = EMS_TYPE_RC10Set; - EMS_TxTelegram.offset = EMS_OFFSET_RC10Set_temp; - EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC10StatusMessage; - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + else if (model == EMS_DEVICE_FLAG_RC20) { + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; + EMS_TxTelegram.type = EMS_TYPE_RC20Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_temp; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + } - } else if (model == EMS_DEVICE_FLAG_RC30) { - EMS_TxTelegram.type = EMS_TYPE_RC30Set; - EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_temp; - EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + else if (model == EMS_DEVICE_FLAG_RC20N) { + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; + EMS_TxTelegram.type = EMS_TYPE_RC20NSet; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20NStatusMessage; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + switch (temptype) { + case EMS_THERMOSTAT_MODE_NIGHT: // change the night temp + EMS_TxTelegram.offset = EMS_OFFSET_RC20NSet_temp_night; + break; + case EMS_THERMOSTAT_MODE_DAY: // change the day temp + EMS_TxTelegram.offset = EMS_OFFSET_RC20NSet_temp_day; + break; + default: + case EMS_THERMOSTAT_MODE_AUTO: // automatic selection, if no type is defined, we use the standard code + EMS_TxTelegram.offset = (EMS_Thermostat.hc[hc - 1].mode_type == 0) ? EMS_OFFSET_RC20NSet_temp_night : EMS_OFFSET_RC20NSet_temp_day; + break; + } + } - } else if (model == EMS_DEVICE_FLAG_RC300) { + else if (model == EMS_DEVICE_FLAG_RC30) { + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; + EMS_TxTelegram.type = EMS_TYPE_RC30Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_temp; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + } + + else if ((model == EMS_DEVICE_FLAG_RC300) || (model == EMS_DEVICE_FLAG_RC100)) { + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; // check mode to determine offset - if (EMS_Thermostat.hc[hc_num - 1].mode == 1) { // auto - EMS_TxTelegram.offset = 0x08; // auto offset - } else if (EMS_Thermostat.hc[hc_num - 1].mode == 0) { // manuaL - EMS_TxTelegram.offset = 0x0A; // manual offset + if (EMS_Thermostat.hc[hc - 1].mode == 1) { // auto + EMS_TxTelegram.offset = 0x08; // auto offset + } else if (EMS_Thermostat.hc[hc - 1].mode == 0) { // manuaL + EMS_TxTelegram.offset = 0x0A; // manual offset } - if (hc_num == 1) { + if (hc == 1) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; // for 3000 and 1010, e.g. 0B 10 FF (0A | 08) 01 89 2B EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC1; - } else if (hc_num == 2) { + } else if (hc == 2) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 1; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC2; - } else if (hc_num == 3) { + } else if (hc == 3) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 2; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC3; - } else if (hc_num == 4) { + } else if (hc == 4) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 3; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC4; } EMS_TxTelegram.type_validate = EMS_ID_NONE; // validate by reading from a different telegram + } - } else if (model == EMS_DEVICE_FLAG_RC35) { + else if ((model == EMS_DEVICE_FLAG_RC35) || (model == EMS_DEVICE_FLAG_RC30N)) { switch (temptype) { - case 1: // change the night temp - EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_night; + case EMS_THERMOSTAT_MODE_NIGHT: // change the night temp + EMS_Thermostat.hc[hc - 1].nighttemp = temperature * 2; + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_night; break; - case 2: // change the day temp - EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_day; + case EMS_THERMOSTAT_MODE_DAY: // change the day temp + EMS_Thermostat.hc[hc - 1].daytemp = temperature * 2; + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_day; break; - case 3: // change the holiday temp - EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_holiday; + case EMS_THERMOSTAT_MODE_HOLIDAY: // change the holiday temp + EMS_Thermostat.hc[hc - 1].holidaytemp = temperature * 2; + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_holiday; break; default: - case 0: // automatic selection, if no type is defined, we use the standard code - if (EMS_Thermostat.hc[hc_num - 1].day_mode == 0) { - EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_night; - } else if (EMS_Thermostat.hc[hc_num - 1].day_mode == 1) { - EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_day; + case EMS_THERMOSTAT_MODE_AUTO: // automatic selection, if no type is defined, we use the standard code + if (model == EMS_DEVICE_FLAG_RC35) { + switch (EMS_Thermostat.hc[hc - 1].mode) { + case 0: // if in nightmode, change nighttemp, seltemp is set automatically + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_night; + break; + case 1: // if in daymode, change daytemp, seltemp is set automatically + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_day; + break; + default: // in automode change only seltemp + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_seltemp; // https://github.com/proddy/EMS-ESP/issues/310 + break; + } + } else { + EMS_TxTelegram.offset = (EMS_Thermostat.hc[hc - 1].mode_type == 0) ? EMS_OFFSET_RC35Set_temp_night : EMS_OFFSET_RC35Set_temp_day; } + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; // set temperature for immediate publish back break; } - if (hc_num == 1) { + if (hc == 1) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC1; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC1; - } else if (hc_num == 2) { + } else if (hc == 2) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC2; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC2; - } else if (hc_num == 3) { + } else if (hc == 3) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC3; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC3; - } else if (hc_num == 4) { + } else if (hc == 4) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC4; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC4; } EMS_TxTelegram.type_validate = EMS_TxTelegram.type; } + else if ((model == EMS_DEVICE_FLAG_JUNKERS1) || (model == EMS_DEVICE_FLAG_JUNKERS2)) { + EMS_TxTelegram.emsplus = true; // Assuming here that all Junkers use EMS+ + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; + + // figure out if we have older or new thermostats + // Heating Circuits on 0x65 or 0x79 + // see https://github.com/proddy/EMS-ESP/issues/335#issuecomment-593324716) + if (model == EMS_DEVICE_FLAG_JUNKERS1) { + switch (temptype) { + case EMS_THERMOSTAT_MODE_NOFROST: + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage_no_frost_temp; + break; + case EMS_THERMOSTAT_MODE_NIGHT: + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage_night_temp; + break; + case EMS_THERMOSTAT_MODE_DAY: + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage_day_temp; + break; + default: + case EMS_THERMOSTAT_MODE_AUTO: // automatic selection, if no type is defined, we use the standard code + EMS_TxTelegram.offset = + (EMS_Thermostat.hc[hc - 1].mode_type == 0) ? EMS_OFFSET_JunkersSetMessage_night_temp : EMS_OFFSET_JunkersSetMessage_day_temp; + break; + } + EMS_TxTelegram.type = EMS_TYPE_JunkersSetMessage1_HC1 + hc - 1; // 0x65 + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_JunkersStatusMessage_HC1 + hc - 1; + } else { + EMS_Thermostat.hc[hc - 1].setpoint_roomTemp = temperature * 2; + // EMS_DEVICE_FLAG_JUNKERS2 + switch (temptype) { + case EMS_THERMOSTAT_MODE_NOFROST: + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage2_no_frost_temp; + break; + case EMS_THERMOSTAT_MODE_ECO: + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage2_eco_temp; + break; + default: + case EMS_THERMOSTAT_MODE_HEAT: + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage3_heat; + break; + } + // older junkers models like the FR100 + EMS_TxTelegram.type = EMS_TYPE_JunkersSetMessage2_HC1 + hc - 1; // 0x79 + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_JunkersStatusMessage_HC1 + hc - 1; + } + + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + } + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value * 2 EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; @@ -2271,13 +2627,84 @@ void ems_setThermostatTemp(float temperature, uint8_t hc_num, uint8_t temptype) EMS_TxQueue.push(EMS_TxTelegram); } +// takes a thermostat mode string and returns its enum value +_EMS_THERMOSTAT_MODE ems_getThermostatMode(const char * mode_s) { + if (strncmp(mode_s, EMS_THERMOSTAT_MODE_AUTO_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_AUTO; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_DAY_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_DAY; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_MANUAL_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_MANUAL; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_HEAT_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_HEAT; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_NIGHT_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_NIGHT; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_OFF_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_OFF; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_COMFORT_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_COMFORT; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_HOLIDAY_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_HOLIDAY; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_NOFROST_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_NOFROST; + } else if (strncmp(mode_s, EMS_THERMOSTAT_MODE_ECO_STR, 10) == 0) { + return EMS_THERMOSTAT_MODE_ECO; + } + + return EMS_THERMOSTAT_MODE_UNKNOWN; +} + +// takes a thermostat mode value and returns its string name +char * ems_getThermostatModeString(_EMS_THERMOSTAT_MODE mode, char * mode_str) { + switch (mode) { + case EMS_THERMOSTAT_MODE_AUTO: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_AUTO_STR, 10); + break; + case EMS_THERMOSTAT_MODE_NIGHT: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_NIGHT_STR, 10); + break; + case EMS_THERMOSTAT_MODE_DAY: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_DAY_STR, 10); + break; + case EMS_THERMOSTAT_MODE_HOLIDAY: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_HOLIDAY_STR, 10); + break; + case EMS_THERMOSTAT_MODE_NOFROST: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_NOFROST_STR, 10); + break; + case EMS_THERMOSTAT_MODE_ECO: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_ECO_STR, 10); + break; + case EMS_THERMOSTAT_MODE_HEAT: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_HEAT_STR, 10); + break; + case EMS_THERMOSTAT_MODE_OFF: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_OFF_STR, 10); + break; + case EMS_THERMOSTAT_MODE_MANUAL: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_MANUAL_STR, 10); + break; + case EMS_THERMOSTAT_MODE_UNKNOWN: + default: + strlcpy(mode_str, EMS_THERMOSTAT_MODE_UNKNOWN_STR, 10); + break; + } + + return (mode_str); +} + +// wrapper for setting thermostat mode, taking a string as an argument +void ems_setThermostatMode(const char * mode_s, uint8_t hc) { + ems_setThermostatMode(ems_getThermostatMode(mode_s), hc); +} + /** * Set the thermostat working mode * 0xA8 on a RC20 and 0xA7 on RC30 * 0x01B9 for EMS+ 300/1000/3000, Auto=0xFF Manual=0x00. See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats * hc_num is 1 to 4 */ -void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { +void ems_setThermostatMode(_EMS_THERMOSTAT_MODE mode, uint8_t hc) { if (!ems_getThermostatEnabled()) { myDebug_P(PSTR("Thermostat not online.")); return; @@ -2288,33 +2715,69 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { return; } - if (hc_num < 1 || hc_num > EMS_THERMOSTAT_MAXHC) { + if (hc < 1 || hc > EMS_THERMOSTAT_MAXHC) { myDebug_P(PSTR("Invalid HC number")); return; } - uint8_t model = ems_getThermostatModel(); - uint8_t device_id = EMS_Thermostat.device_id; - uint8_t set_mode; + // set the value to send via EMS depending on the mode type + uint8_t set_mode_value = 0x02; // default is 2 which is usually auto + switch (mode) { + case EMS_THERMOSTAT_MODE_NIGHT: + case EMS_THERMOSTAT_MODE_OFF: + set_mode_value = 0; + break; - // RC300/1000/3000 have different settings - if (model == EMS_DEVICE_FLAG_RC300) { - if (mode == 1) { - set_mode = 0; // manual - } else { - set_mode = 0xFF; // auto - } - } else { - set_mode = mode; + case EMS_THERMOSTAT_MODE_DAY: + case EMS_THERMOSTAT_MODE_HEAT: + case EMS_THERMOSTAT_MODE_MANUAL: + case EMS_THERMOSTAT_MODE_NOFROST: + set_mode_value = 1; + break; + + default: + case EMS_THERMOSTAT_MODE_AUTO: + case EMS_THERMOSTAT_MODE_HOLIDAY: + case EMS_THERMOSTAT_MODE_ECO: + case EMS_THERMOSTAT_MODE_UNKNOWN: + set_mode_value = 2; + break; } - // 0=off, 1=manual, 2=auto - if (mode == 0) { - myDebug_P(PSTR("Setting thermostat mode to off for heating circuit %d"), hc_num); - } else if (set_mode == 1) { - myDebug_P(PSTR("Setting thermostat mode to manual for heating circuit %d"), hc_num); - } else if (set_mode == 2) { - myDebug_P(PSTR("Setting thermostat mode to auto for heating circuit %d"), hc_num); + // now override the mode value setting depending on the thermostat type + // handle the different mode values to send per thermostat type + uint8_t model = ems_getThermostatFlags(); + switch (model) { + case EMS_DEVICE_FLAG_RC300: + case EMS_DEVICE_FLAG_RC100: + if (mode == EMS_THERMOSTAT_MODE_AUTO) { + set_mode_value = 0xFF; // special value for auto + } else { + set_mode_value = 0; // everything else, like manual/day etc.. + } + break; + case EMS_DEVICE_FLAG_JUNKERS1: + case EMS_DEVICE_FLAG_JUNKERS2: + if (mode == EMS_THERMOSTAT_MODE_NOFROST) { + set_mode_value = 0x01; + } else if (mode == EMS_THERMOSTAT_MODE_ECO) { + set_mode_value = 0x02; + } else if ((mode == EMS_THERMOSTAT_MODE_DAY) || (mode == EMS_THERMOSTAT_MODE_HEAT)) { + set_mode_value = 0x03; // comfort + } else if (mode == EMS_THERMOSTAT_MODE_AUTO) { + set_mode_value = 0x04; + } + break; + default: + break; + } + + char mode_str[10]; + ems_getThermostatModeString(mode, mode_str); // get text name of mode + if (hc == 1) { + myDebug_P(PSTR("Setting thermostat mode to %s"), mode_str); + } else { + myDebug_P(PSTR("Setting thermostat mode to %s for heating circuit %d"), mode_str, hc); } _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx @@ -2322,9 +2785,9 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { EMS_Sys_Status.txRetryCount = 0; // reset retry counter EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = device_id; + EMS_TxTelegram.dest = EMS_Thermostat.device_id; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.dataValue = set_mode; + EMS_TxTelegram.dataValue = set_mode_value; // handle different thermostat types if (model == EMS_DEVICE_FLAG_RC20) { @@ -2333,42 +2796,63 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { EMS_TxTelegram.type_validate = EMS_TYPE_RC20Set; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage; + } else if (model == EMS_DEVICE_FLAG_RC20N) { + EMS_TxTelegram.type = EMS_TYPE_RC20NSet; + EMS_TxTelegram.offset = EMS_OFFSET_RC20NSet_mode; + EMS_TxTelegram.type_validate = EMS_TYPE_RC20NSet; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20NStatusMessage; + } else if (model == EMS_DEVICE_FLAG_RC30) { EMS_TxTelegram.type = EMS_TYPE_RC30Set; EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_mode; EMS_TxTelegram.type_validate = EMS_TYPE_RC30Set; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; - } else if (model == EMS_DEVICE_FLAG_RC35) { - if (hc_num == 1) { + } else if ((model == EMS_DEVICE_FLAG_RC35) || (model == EMS_DEVICE_FLAG_RC30N)) { + if (hc == 1) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC1; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC1; - } else if (hc_num == 2) { + } else if (hc == 2) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC2; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC2; - } else if (hc_num == 3) { + } else if (hc == 3) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC3; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC3; - } else if (hc_num == 4) { + } else if (hc == 4) { EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC4; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC4; } EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_mode; EMS_TxTelegram.type_validate = EMS_TxTelegram.type; - } else if (model == EMS_DEVICE_FLAG_RC300) { + // Junkers + } else if (model == EMS_DEVICE_FLAG_JUNKERS1) { + EMS_TxTelegram.emsplus = true; // Assuming here that all Junkers use EMS+ + EMS_TxTelegram.type = EMS_TYPE_JunkersSetMessage1_HC1 + hc - 1; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_JunkersStatusMessage_HC1; + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage_set_mode; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + } else if (model == EMS_DEVICE_FLAG_JUNKERS2) { + EMS_TxTelegram.emsplus = true; // Assuming here that all Junkers use EMS+ + EMS_TxTelegram.type = EMS_TYPE_JunkersSetMessage2_HC1 + hc - 1; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_JunkersStatusMessage_HC1; + EMS_TxTelegram.offset = EMS_OFFSET_JunkersSetMessage_set_mode; + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + } + + else if ((model == EMS_DEVICE_FLAG_RC300) || (model == EMS_DEVICE_FLAG_RC100)) { EMS_TxTelegram.offset = EMS_OFFSET_RCPLUSSet_mode; - if (hc_num == 1) { + if (hc == 1) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC1; - } else if (hc_num == 2) { + } else if (hc == 2) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 1; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC2; - } else if (hc_num == 3) { + } else if (hc == 3) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 2; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC3; - } else if (hc_num == 4) { + } else if (hc == 4) { EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet + 3; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage_HC4; } @@ -2382,17 +2866,114 @@ void ems_setThermostatMode(uint8_t mode, uint8_t hc_num) { EMS_TxQueue.push(EMS_TxTelegram); } +/** + * Set the language settings + * to 0xA5 + */ +void ems_setSettingsLanguage(uint8_t lg) { + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter + + switch (lg) { + case EMS_VALUE_IBASettings_LANG_FRENCH: + case EMS_VALUE_IBASettings_LANG_GERMAN: + case EMS_VALUE_IBASettings_LANG_DUTCH: + case EMS_VALUE_IBASettings_LANG_ITALIAN: + myDebug_P(PSTR("Setting language to %d"), lg); + EMS_TxTelegram.dataValue = lg; + break; + default: + return; // invalid value + } + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = EMS_Thermostat.device_id; + EMS_TxTelegram.type = EMS_TYPE_IBASettingsMessage; + EMS_TxTelegram.offset = EMS_OFFSET_IBASettings_Language; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + + EMS_TxQueue.push(EMS_TxTelegram); +} + +/** + * Set the building settings + * to 0xA5 + */ +void ems_setSettingsBuilding(uint8_t bg) { + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter + + switch (bg) { + case EMS_VALUE_IBASettings_BUILDING_LIGHT: + case EMS_VALUE_IBASettings_BUILDING_MEDIUM: + case EMS_VALUE_IBASettings_BUILDING_HEAVY: + myDebug_P(PSTR("Setting building to %d"), bg); + EMS_TxTelegram.dataValue = bg; + break; + default: + return; // invalid value + } + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = EMS_Thermostat.device_id; + EMS_TxTelegram.type = EMS_TYPE_IBASettingsMessage; + EMS_TxTelegram.offset = EMS_OFFSET_IBASettings_Building; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + + EMS_TxQueue.push(EMS_TxTelegram); +} + +/** + * Set the display settings + * to 0xA5 + */ +void ems_setSettingsDisplay(uint8_t ds) { + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter + + switch (ds) { + case EMS_VALUE_IBASettings_DISPLAY_INTTEMP: + case EMS_VALUE_IBASettings_DISPLAY_INTSETPOINT: + case EMS_VALUE_IBASettings_DISPLAY_EXTTEMP: + case EMS_VALUE_IBASettings_DISPLAY_BURNERTEMP: + case EMS_VALUE_IBASettings_DISPLAY_WWTEMP: + case EMS_VALUE_IBASettings_DISPLAY_FUNCMODE: + case EMS_VALUE_IBASettings_DISPLAY_TIME: + case EMS_VALUE_IBASettings_DISPLAY_DATE: + case EMS_VALUE_IBASettings_DISPLAY_SMOKETEMP: + myDebug_P(PSTR("Setting display to %d"), ds); + EMS_TxTelegram.dataValue = ds; + break; + default: + return; // invalid value + } + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = EMS_Thermostat.device_id; + EMS_TxTelegram.type = EMS_TYPE_IBASettingsMessage; + EMS_TxTelegram.offset = EMS_OFFSET_IBASettings_Display; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + + EMS_TxQueue.push(EMS_TxTelegram); +} + /** * Set the warm water temperature 0x33 */ void ems_setWarmWaterTemp(uint8_t temperature) { // check for invalid temp values - if ((temperature < 30) || (temperature > EMS_BOILER_TAPWATER_TEMPERATURE_MAX)) { + if ((temperature < EMS_BOILER_TAPWATER_TEMPERATURE_MIN) || (temperature > EMS_BOILER_TAPWATER_TEMPERATURE_MAX)) { return; } myDebug_P(PSTR("Setting boiler warm water temperature to %d C"), temperature); - + EMS_Boiler.wWSelTemp = temperature; _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter @@ -2429,7 +3010,9 @@ void ems_setFlowTemp(uint8_t temperature) { EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; EMS_TxTelegram.dataValue = temperature; // int value to compare against - EMS_TxTelegram.type_validate = EMS_TYPE_UBASetPoints; // validate + // EMS_TxTelegram.type_validate = EMS_TYPE_UBASetPoints; // validate + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate after the write + EMS_TxTelegram.comparisonOffset = EMS_OFFSET_UBASetPoints_flowtemp; EMS_TxTelegram.comparisonValue = temperature; EMS_TxTelegram.comparisonPostRead = EMS_TYPE_UBASetPoints; @@ -2440,6 +3023,7 @@ void ems_setFlowTemp(uint8_t temperature) { /** * Set the warm water mode to comfort to Eco/Comfort * 1 = Hot, 2 = Eco, 3 = Intelligent + * to 0x33 */ void ems_setWarmWaterModeComfort(uint8_t comfort) { _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx @@ -2485,8 +3069,14 @@ void ems_setWarmWaterActivated(bool activated) { EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwactivated; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate - EMS_TxTelegram.dataValue = (activated ? 0xFF : 0x00); // 0xFF is on, 0x00 is off + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + + // https://github.com/proddy/EMS-ESP/issues/268 + if (ems_isHT3()) { + EMS_TxTelegram.dataValue = (activated ? 0x08 : 0x00); // 0x08 is on, 0x00 is off + } else { + EMS_TxTelegram.dataValue = (activated ? 0xFF : 0x00); // 0xFF is on, 0x00 is off + } EMS_TxQueue.push(EMS_TxTelegram); } @@ -2519,13 +3109,13 @@ void ems_setWarmTapWaterActivated(bool activated) { EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.type; // create header - EMS_TxTelegram.data[0] = EMS_ID_ME; // src - EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; // dest - EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type - EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; // offset + EMS_TxTelegram.data[0] = EMS_Sys_Status.emsbusid; // src + EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; // dest + EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type + EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; // offset // we use the special test mode 0x1D for this. Setting the first data to 5A puts the system into test mode and - // a setting of 0x00 puts it back into normal operarting mode + // a setting of 0x00 puts it back into normal operating mode // when in test mode we're able to mess around with the 3-way valve settings if (!activated) { // on @@ -2557,11 +3147,11 @@ void ems_startupTelegrams() { char s[20] = {0}; // Write type 0x1D to get out of function test mode - snprintf(s, sizeof(s), "%02X %02X 1D 00 00", EMS_ID_ME, EMS_Boiler.device_id); + snprintf(s, sizeof(s), "%02X %02X 1D 00 00", EMS_Sys_Status.emsbusid, EMS_Boiler.device_id); ems_sendRawTelegram(s); // Read type 0x01 - snprintf(s, sizeof(s), "%02X %02X 01 00 1B", EMS_ID_ME, EMS_Boiler.device_id | 0x80); + snprintf(s, sizeof(s), "%02X %02X 01 00 1B", EMS_Sys_Status.emsbusid, EMS_Boiler.device_id | 0x80); ems_sendRawTelegram(s); } @@ -2596,7 +3186,7 @@ void ems_testTelegram(uint8_t test_num) { telegram[0] = (uint8_t)strtol(value, 0, 16); } - // and interate until end + // and iterate until end while (p != 0) { if ((p = strtok(nullptr, " ,"))) { strlcpy(value, p, sizeof(value)); @@ -2622,13 +3212,16 @@ void ems_testTelegram(uint8_t test_num) { */ const _EMS_Type EMS_Types[] = { + // types that we know about but don't have handlers yet + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_UBAFlags, "UBAFlags", nullptr}, + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_UBAMaintenanceStatusMessage, "UBAMaintenanceStatusMessage", nullptr}, + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_MC10Status, "MC10Status", nullptr}, + // common {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_Version, "Version", _process_Version}, - {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_UBADevices, "UBADevices", _process_UBADevices}, - {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, // UBA/Boiler + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_UBADevices, "UBADevices", _process_UBADevices}, {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", _process_UBAMonitorFast}, {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", _process_UBAMonitorSlow}, {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage}, @@ -2642,7 +3235,7 @@ const _EMS_Type EMS_Types[] = { {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorFast2, "UBAMonitorFast2", _process_UBAMonitorFast2}, {EMS_DEVICE_UPDATE_FLAG_BOILER, EMS_TYPE_UBAMonitorSlow2, "UBAMonitorSlow2", _process_UBAMonitorSlow2}, - // Solar Module devices + // Solar Module devices. Note SM100 also covers SM200 {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM10Monitor, "SM10Monitor", _process_SM10Monitor}, {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM100Monitor, "SM100Monitor", _process_SM100Monitor}, {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_SM100Status, "SM100Status", _process_SM100Status}, @@ -2651,10 +3244,14 @@ const _EMS_Type EMS_Types[] = { {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_ISM1StatusMessage, "ISM1StatusMessage", _process_ISM1StatusMessage}, {EMS_DEVICE_UPDATE_FLAG_SOLAR, EMS_TYPE_ISM1Set, "ISM1Set", _process_ISM1Set}, - // heatpumps + // heat pumps {EMS_DEVICE_UPDATE_FLAG_HEATPUMP, EMS_TYPE_HPMonitor1, "HeatPumpMonitor1", _process_HPMonitor1}, {EMS_DEVICE_UPDATE_FLAG_HEATPUMP, EMS_TYPE_HPMonitor2, "HeatPumpMonitor2", _process_HPMonitor2}, + // Thermostats... + {EMS_DEVICE_UPDATE_FLAG_NONE, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + // RC10 {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC10Set, "RC10Set", _process_RC10Set}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC10StatusMessage, "RC10StatusMessage", _process_RC10StatusMessage}, @@ -2663,6 +3260,10 @@ const _EMS_Type EMS_Types[] = { {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, + // RC20 and ES72 + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC20NSet, "RC20NSet", _process_RC20NSet}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC20NStatusMessage, "RC20NStatusMessage", _process_RC20NStatusMessage}, + // RC30 {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC30Set, "RC30Set", _process_RC30Set}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC30StatusMessage, "RC30StatusMessage", _process_RC30StatusMessage}, @@ -2672,7 +3273,7 @@ const _EMS_Type EMS_Types[] = { {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC1, "RC35StatusMessage_HC1", _process_RC35StatusMessage}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC2, "RC35Set_HC2", _process_RC35Set}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC2, "RC35StatusMessage_HC2", _process_RC35StatusMessage}, - {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC3, "RC35Set_HC2", _process_RC35Set}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC3, "RC35Set_HC3", _process_RC35Set}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC3, "RC35StatusMessage_HC3", _process_RC35StatusMessage}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35Set_HC4, "RC35Set_HC4", _process_RC35Set}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RC35StatusMessage_HC4, "RC35StatusMessage_HC4", _process_RC35StatusMessage}, @@ -2680,7 +3281,7 @@ const _EMS_Type EMS_Types[] = { // Easy {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, - // Nefit 1010, RC300, RC310 (EMS Plus) + // Nefit 1010, RC300, RC100, RC310 (EMS Plus) {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMessage_HC1, "RCPLUSStatusMessage_HC1", _process_RCPLUSStatusMessage}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMessage_HC2, "RCPLUSStatusMessage_HC2", _process_RCPLUSStatusMessage}, {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMessage_HC3, "RCPLUSStatusMessage_HC3", _process_RCPLUSStatusMessage}, @@ -2689,13 +3290,22 @@ const _EMS_Type EMS_Types[] = { {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_RCPLUSStatusMode, "RCPLUSStatusMode", _process_RCPLUSStatusMode}, // Junkers FR10 - {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_JunkersStatusMessage, "JunkersStatusMessage", _process_JunkersStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_JunkersStatusMessage_HC1, "JunkersStatusMessage_HC1", _process_JunkersStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_JunkersStatusMessage_HC2, "JunkersStatusMessage_HC2", _process_JunkersStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_JunkersStatusMessage_HC3, "JunkersStatusMessage_HC3", _process_JunkersStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_JunkersStatusMessage_HC4, "JunkersStatusMessage_HC4", _process_JunkersStatusMessage}, - // Mixing devices + // settings + {EMS_DEVICE_UPDATE_FLAG_THERMOSTAT, EMS_TYPE_IBASettingsMessage, "IBASettingsMessage", _process_IBASettingsMessage}, + + // Mixing devices MM10 - MM400 {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_HC1, "MMPLUSStatusMessage_HC1", _process_MMPLUSStatusMessage}, - {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_HC2, "MMPLUSStatusMessage_HC2", _process_MMPLUSStatusMessage} - -}; + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_HC2, "MMPLUSStatusMessage_HC2", _process_MMPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_HC3, "MMPLUSStatusMessage_HC3", _process_MMPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_HC4, "MMPLUSStatusMessage_HC4", _process_MMPLUSStatusMessage}, + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_WWC1, "MMPLUSStatusMessage_WWC1", _process_MMPLUSStatusMessageWW}, + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMPLUSStatusMessage_WWC2, "MMPLUSStatusMessage_WWC2", _process_MMPLUSStatusMessageWW}, + {EMS_DEVICE_UPDATE_FLAG_MIXING, EMS_TYPE_MMStatusMessage, "MMStatusMessage", _process_MMStatusMessage}}; // calculate sizes of arrays at compile time uint8_t _EMS_Types_max = ArraySize(EMS_Types); @@ -2705,6 +3315,10 @@ uint8_t _EMS_Types_max = ArraySize(EMS_Types); * or -1 if not found */ int8_t _ems_findType(uint16_t type) { + if (type == 0) { + return -1; + } + uint8_t i = 0; bool typeFound = false; // scan through known ID types @@ -2719,46 +3333,138 @@ int8_t _ems_findType(uint16_t type) { return (typeFound ? i : -1); } +/** + * print the telegram + */ +void _printMessage(_EMS_RxTelegram * EMS_RxTelegram, const int8_t show_type) { + // only print if we have logging enabled + if (EMS_Sys_Status.emsLogging < EMS_SYS_LOGGING_THERMOSTAT) { + return; + } + + // header info + 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 color_s[20] = {0}; + char type_s[30]; + + // source + (void)ems_getDeviceTypeDescription(src, type_s); + strlcpy(output_str, type_s, sizeof(output_str)); + strlcat(output_str, " -> ", sizeof(output_str)); + + // destination + (void)ems_getDeviceTypeDescription(dest, type_s); + strlcat(output_str, type_s, sizeof(output_str)); + + if (dest == EMS_Sys_Status.emsbusid) { + strlcpy(color_s, COLOR_YELLOW, sizeof(color_s)); // me + } else if (dest == EMS_ID_NONE) { + strlcpy(color_s, COLOR_GREEN, sizeof(color_s)); // broadcast + } else { + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); // everything else + } + + // print type + // if we're been given an index to the type string, use that + if (length) { + char buffer[16]; + strlcat(output_str, ", ", sizeof(output_str)); + if (show_type != -1) { + strlcat(output_str, EMS_Types[show_type].typeString, sizeof(output_str)); + } else { + strlcat(output_str, "Type", sizeof(output_str)); + } + strlcat(output_str, "(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)); + } + strlcat(output_str, ")", 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 solar module if logging is set to solar module only + if ((src == EMS_SolarModule.device_id) || (dest == EMS_SolarModule.device_id)) { + _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); + } + } else if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_MIXINGMODULE) { + // only print ones to/from mixing module if logging is set to mixing module only + if ((src == EMS_MixingModule.device_id) || (dest == EMS_MixingModule.device_id)) { + _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); + // also analyse the sequence of instructions prior to instructions to/from mixing module + // typically: EMS_TYPE_MM10ParameterMessage(0xAC) - EMS_TYPE_UBASetPoints(0x1A) - EMS_TYPE_UBAFlags(0x35) + } else if ((type == EMS_TYPE_MMStatusMessage) || (type == EMS_TYPE_MM10ParameterMessage) || (type == EMS_TYPE_UBASetPoints) + || (type == EMS_TYPE_UBAFlags)) { + _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); + } + } else if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_DEVICE) { + // only print ones to/from DeviceID + if ((src == EMS_Sys_Status.emsLogging_ID) || (dest == EMS_Sys_Status.emsLogging_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 */ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { - // print out the telegram for verbose mode - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { - _printMessage(EMS_RxTelegram); - } + // first see if we know which type this. -1 if not found. + uint16_t type = EMS_RxTelegram->type; + int8_t type_index = _ems_findType(type); // ignore telegrams that don't have any data if (EMS_RxTelegram->data_length == 0) { + _printMessage(EMS_RxTelegram, type_index); return; } - // we're only interested in broadcast messages (dest is 0x00) or ones for us (dest is 0x0B) + // we're only interested in broadcast messages (dest is 0x00) or ones sent to us uint8_t dest = EMS_RxTelegram->dest; - if ((dest != EMS_ID_NONE) && (dest != EMS_ID_ME)) { + if ((dest != EMS_ID_NONE) && (dest != EMS_Sys_Status.emsbusid)) { + _printMessage(EMS_RxTelegram, type_index); return; } - // see if we recognize the type first by scanning our known EMS types list - uint16_t type = EMS_RxTelegram->type; - int8_t i = _ems_findType(type); - if (i == -1) { - return; // not found + // we have a matching type ID, print the detailed telegram to the console + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) { + if (type == -1) { + myDebug_P(PSTR("<--- Type(0x%02X)"), type); + } else { + myDebug_P(PSTR("<--- %s(0x%02X)"), EMS_Types[type_index].typeString, type); + } + } else { + _printMessage(EMS_RxTelegram, type_index); // print with index to the Type string } - // 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 ((EMS_Types[i].processType_cb) != nullptr) { - // print non-verbose message - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) { - myDebug_P(PSTR("<--- %s(0x%02X)"), EMS_Types[i].typeString, type); - } - // call callback function to process the telegram - (void)EMS_Types[i].processType_cb(EMS_RxTelegram); + // quit if we don't know how to handle this type + if (type_index == -1) { + return; + } - // see if we need to flag something has changed - ems_Device_add_flags(EMS_Types[i].device_flag); + // process it by calling its respective callback function + if ((EMS_Types[type_index].processType_cb) != nullptr) { + (void)EMS_Types[type_index].processType_cb(EMS_RxTelegram); + ems_Device_add_flags(EMS_Types[type_index].device_flag); // see if we need to flag something has changed } EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; @@ -2773,7 +3479,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t * telegram = EMS_RxTelegram->telegram; // if its an echo of ourselves from the master UBA, ignore. This should never happen mind you - if (EMS_RxTelegram->src == EMS_ID_ME) { + if (EMS_RxTelegram->src == EMS_Sys_Status.emsbusid) { if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) _debugPrintTelegram("echo: ", EMS_RxTelegram, COLOR_WHITE); return; @@ -2791,7 +3497,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { // at this point we can assume TxStatus was EMS_TX_STATUS_WAIT as we just sent a read or validate telegram // for READ or VALIDATE the dest (telegram[1]) is always us, so check for this // and if not we probably didn't get any response so remove the last Tx from the queue and process the telegram anyway - if ((telegram[1] & 0x7F) != EMS_ID_ME) { + if ((telegram[1] & 0x7F) != EMS_Sys_Status.emsbusid) { _removeTxQueue(); _ems_processTelegram(EMS_RxTelegram); return; @@ -2894,7 +3600,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) { * 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) */ -void ems_doReadCommand(uint16_t type, uint8_t dest) { +void ems_doReadCommand(uint16_t type, uint8_t dest, uint8_t offset) { // if not a valid type of boiler is not accessible then quits if ((type == EMS_ID_NONE) || (dest == EMS_ID_NONE)) { return; @@ -2924,7 +3630,7 @@ void ems_doReadCommand(uint16_t type, uint8_t 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.offset = offset; // defaults to 0 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 @@ -2935,3 +3641,33 @@ void ems_doReadCommand(uint16_t type, uint8_t dest) { EMS_TxQueue.push(EMS_TxTelegram); } + +/** + * Find the versions of our connected devices + */ +void ems_scanDevices() { + std::list Device_Ids; // create a new list + + Device_Ids.push_back(EMS_ID_BOILER); // UBAMaster/Boilers - 0x08 + Device_Ids.push_back(0x09); // Controllers - 0x09 + Device_Ids.push_back(0x38); // HeatPump - 0x38 + Device_Ids.push_back(0x30); // Solar Module - 0x30 + Device_Ids.push_back(0x02); // Connect - 0x02 + Device_Ids.push_back(0x48); // Gateway - 0x48 + Device_Ids.push_back(0x20); // Mixing Devices - 0x20 + Device_Ids.push_back(0x21); // Mixing Devices - 0x21 + Device_Ids.push_back(0x10); // Thermostats - 0x10 + Device_Ids.push_back(0x17); // Thermostats - 0x17 + Device_Ids.push_back(0x18); // Remote Thermostats - 0x18 + Device_Ids.push_back(0x19); // Remote Thermostats - 0x19 + Device_Ids.push_back(0x11); // Switch WM10 - 0x11 + + // remove duplicates and reserved IDs (like our own device) + // Device_Ids.sort(); + // Device_Ids.unique(); + + // send the read command with Version command + for (uint8_t device_id : Device_Ids) { + ems_doReadCommand(EMS_TYPE_Version, device_id); + } +} diff --git a/src/ems.h b/src/ems.h index 03f619291..bf8529aca 100644 --- a/src/ems.h +++ b/src/ems.h @@ -12,6 +12,9 @@ #include #include // std::list +// EMS bus IDs +#define EMS_BUSID_DEFAULT 0x0B // Default 0x0B (Service Key) + // EMS tx_mode types #define EMS_TXMODE_DEFAULT 1 // Default (was previously known as tx_mode 2 in v1.8.x) #define EMS_TXMODE_EMSPLUS 2 // EMS+ @@ -26,11 +29,12 @@ #define EMS_VALUE_BOOL_ON 0x01 // boolean true #define EMS_VALUE_BOOL_ON2 0xFF // boolean true, EMS sometimes uses 0xFF for TRUE #define EMS_VALUE_BOOL_OFF 0x00 // boolean false -#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_VALUE_BOOL_NOTSET 0xFE // random number that's not 0, 1 or FF +#define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit unsigned ints/bytes +#define EMS_VALUE_SHORT_NOTSET -32000 // 0x8300 for 2-byte signed shorts +#define EMS_VALUE_USHORT_NOTSET 32000 // 0x7D00 (was 0x8000) for 2-byte unsigned shorts +#define EMS_VALUE_USHORT_NOTVALID 0x8000 // 0x8000 for 2-byte unsigned shorts +#define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs // thermostat specific #define EMS_THERMOSTAT_MAXHC 4 // max number of heating circuits @@ -38,36 +42,40 @@ #define EMS_THERMOSTAT_WRITE_YES true #define EMS_THERMOSTAT_WRITE_NO false +// mixing specific +#define EMS_MIXING_MAXHC 4 // max number of heating circuits +#define EMS_MIXING_MAXWWC 2 // max number of warm water circuits + // Device Flags -#define EMS_DEVICE_FLAG_NONE 0 // no flags set -#define EMS_DEVICE_FLAG_SM10 10 // solar module1 -#define EMS_DEVICE_FLAG_SM100 11 // solar module2 - -// group flags specific for thermostats -#define EMS_DEVICE_FLAG_NO_WRITE 0x80 // top bit set if write not supported -#define EMS_DEVICE_FLAG_EASY 1 -#define EMS_DEVICE_FLAG_RC10 2 -#define EMS_DEVICE_FLAG_RC20 3 -#define EMS_DEVICE_FLAG_RC30 4 -#define EMS_DEVICE_FLAG_RC35 5 -#define EMS_DEVICE_FLAG_RC300 6 -#define EMS_DEVICE_FLAG_JUNKERS 7 - -typedef enum { - EMS_THERMOSTAT_MODE_UNKNOWN, - EMS_THERMOSTAT_MODE_OFF, - EMS_THERMOSTAT_MODE_MANUAL, - EMS_THERMOSTAT_MODE_AUTO, - EMS_THERMOSTAT_MODE_NIGHT, - EMS_THERMOSTAT_MODE_DAY -} _EMS_THERMOSTAT_MODE; +// They are unique to the model type (mixing, solar, thermostat etc) +enum EMS_DEVICE_FLAG_TYPES : uint8_t { + EMS_DEVICE_FLAG_NONE = 0, + EMS_DEVICE_FLAG_MMPLUS = 20, // mixing EMS+ + EMS_DEVICE_FLAG_MM10 = 21, // mixing MM10 + EMS_DEVICE_FLAG_SM10 = 10, + EMS_DEVICE_FLAG_SM100 = 11, // for SM100 and SM200 + EMS_DEVICE_FLAG_EASY = 1, + EMS_DEVICE_FLAG_RC10 = 2, + EMS_DEVICE_FLAG_RC20 = 3, + EMS_DEVICE_FLAG_RC30 = 4, + EMS_DEVICE_FLAG_RC30N = 5, // newer type of RC30 with RC35 circuit + EMS_DEVICE_FLAG_RC35 = 6, + EMS_DEVICE_FLAG_RC100 = 7, + EMS_DEVICE_FLAG_RC300 = 8, + EMS_DEVICE_FLAG_RC20N = 9, + EMS_DEVICE_FLAG_JUNKERS1 = 31, // use 0x65 for HC + EMS_DEVICE_FLAG_JUNKERS2 = 32, // use 0x79 for HC, older models + EMS_DEVICE_FLAG_JUNKERS = (1 << 6), // 6th bit set if its junkers HT3 + EMS_DEVICE_FLAG_NO_WRITE = (1 << 7) // top bit set if thermostat write not supported +}; // trigger settings to determine if hot tap water or the heating is active #define EMS_BOILER_BURNPOWER_TAPWATER 100 -#define EMS_BOILER_SELFLOWTEMP_HEATING 30 // was 70, changed to 30 for https://github.com/proddy/EMS-ESP/issues/193 +#define EMS_BOILER_SELFLOWTEMP_HEATING 20 // was originally 70, changed to 30 for issue #193, then to 20 with issue #344 -// define maximum setable tapwater temperature +// define min & maximum setable tapwater temperatures #define EMS_BOILER_TAPWATER_TEMPERATURE_MAX 60 +#define EMS_BOILER_TAPWATER_TEMPERATURE_MIN 30 #define EMS_TX_TELEGRAM_QUEUE_MAX 50 // max size of Tx FIFO queue. Number of Tx records to send. @@ -106,55 +114,42 @@ typedef enum { /* EMS logging */ typedef enum { - EMS_SYS_LOGGING_NONE, // no messages - EMS_SYS_LOGGING_RAW, // raw data mode - EMS_SYS_LOGGING_WATCH, // watch a specific type ID - 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_JABBER // lots of debug output... + EMS_SYS_LOGGING_NONE, // no messages + EMS_SYS_LOGGING_RAW, // raw data mode + EMS_SYS_LOGGING_WATCH, // watch a specific type ID + 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 solar module + EMS_SYS_LOGGING_MIXINGMODULE, // only telegrams sent from mixing module + EMS_SYS_LOGGING_VERBOSE, // everything + EMS_SYS_LOGGING_JABBER, // lots of debug output... + EMS_SYS_LOGGING_DEVICE // watch the device ID } _EMS_SYS_LOGGING; // status/counters since last power on typedef struct { _EMS_RX_STATUS emsRxStatus; _EMS_TX_STATUS emsTxStatus; - uint16_t emsRxPgks; // # successfull received - uint16_t emsTxPkgs; // # successfull sent - uint16_t emxCrcErr; // CRC errors - bool emsPollEnabled; // flag enable the response to poll messages - _EMS_SYS_LOGGING emsLogging; // logging - uint16_t emsLogging_typeID; // the typeID to watch - uint8_t emsRefreshedFlags; // fresh data, needs to be pushed out to MQTT - bool emsBusConnected; // is there an active bus - uint32_t emsRxTimestamp; // timestamp of last EMS message received - uint32_t emsPollFrequency; // time between EMS polls - bool emsTxCapable; // able to send via Tx - bool emsTxDisabled; // true to prevent all Tx - uint8_t txRetryCount; // # times the last Tx was re-sent - uint8_t emsIDMask; // Buderus: 0x00, Junkers: 0x80 - uint8_t emsPollAck[1]; // acknowledge buffer for Poll - uint8_t emsTxMode; // Tx mode 1, 2 or 3 - char emsDeviceMap[EMS_SYS_DEVICEMAP_LENGTH]; // contents of 0x07 telegram with bitmasks for all active EMS devices + uint16_t emsRxPgks; // # successfull received + uint16_t emsTxPkgs; // # successfull sent + uint16_t emxCrcErr; // CRC errors + bool emsPollEnabled; // flag enable the response to poll messages + _EMS_SYS_LOGGING emsLogging; // logging + uint16_t emsLogging_ID; // the type or device ID to watch + uint8_t emsRefreshedFlags; // fresh data, needs to be pushed out to MQTT + bool emsBusConnected; // is there an active bus + uint32_t emsRxTimestamp; // timestamp of last EMS message received + uint32_t emsPollFrequency; // time between EMS polls + bool emsTxCapable; // able to send via Tx + bool emsTxDisabled; // true to prevent all Tx + uint8_t txRetryCount; // # times the last Tx was re-sent + uint8_t emsIDMask; // Buderus: 0x00, Junkers: 0x80 + uint8_t emsPollAck[1]; // acknowledge buffer for Poll + uint8_t emsTxMode; // Tx mode 1, 2 or 3 + uint8_t emsbusid; // EMS bus ID, default 0x0B for Service Key + uint8_t emsMasterThermostat; // product ID for the default thermostat to use } _EMS_Sys_Status; -// The Tx send package -typedef struct { - _EMS_TX_TELEGRAM_ACTION action; // read, write, validate, init - uint8_t dest; - uint16_t type; - uint8_t offset; - uint8_t length; // full length of complete telegram, including CRC - uint8_t dataValue; // value to validate against - uint16_t type_validate; // type to call after a successful Write command - uint8_t comparisonValue; // value to compare against during a validate command - uint8_t comparisonOffset; // offset of where the byte is we want to compare too during validation - uint16_t comparisonPostRead; // after a successful write, do a read from this type ID - unsigned long timestamp; // when created - uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; -} _EMS_TxTelegram; - // The Rx receive package typedef struct { unsigned long timestamp; // timestamp from millis() @@ -170,6 +165,23 @@ typedef struct { uint8_t emsplus_type; // FF, F7 or F9 } _EMS_RxTelegram; +// The Tx send package +typedef struct { + _EMS_TX_TELEGRAM_ACTION action; // read, write, validate, init + uint8_t dest; + uint16_t type; + uint8_t offset; + uint8_t length; // full length of complete telegram, including CRC + bool emsplus; // true if ems+/ems 2.0 + uint8_t dataValue; // value to validate against + uint16_t type_validate; // type to call after a successful Write command + uint8_t comparisonValue; // value to compare against during a validate command + uint8_t comparisonOffset; // offset of where the byte is we want to compare too during validation + uint16_t comparisonPostRead; // after a successful write, do a read from this type ID + unsigned long timestamp; // when created + uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; +} _EMS_TxTelegram; + // default empty Tx, must match struct const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { EMS_TX_TELEGRAM_INIT, // action @@ -177,7 +189,8 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { EMS_ID_NONE, // type 0, // offset 0, // length - 0, // data value + false, // emsplus (no) + 0, // dataValue EMS_ID_NONE, // type_validate 0, // comparisonValue 0, // comparisonOffset @@ -193,7 +206,8 @@ typedef enum : uint8_t { EMS_DEVICE_UPDATE_FLAG_THERMOSTAT = (1 << 1), EMS_DEVICE_UPDATE_FLAG_MIXING = (1 << 2), EMS_DEVICE_UPDATE_FLAG_SOLAR = (1 << 3), - EMS_DEVICE_UPDATE_FLAG_HEATPUMP = (1 << 4) + EMS_DEVICE_UPDATE_FLAG_HEATPUMP = (1 << 4), + EMS_DEVICE_UPDATE_FLAG_SETTINGS = (1 << 5) } _EMS_DEVICE_UPDATE_FLAG; typedef enum : uint8_t { @@ -205,13 +219,36 @@ typedef enum : uint8_t { EMS_DEVICE_TYPE_SOLAR, EMS_DEVICE_TYPE_HEATPUMP, EMS_DEVICE_TYPE_GATEWAY, - EMS_DEVICE_TYPE_OTHER, EMS_DEVICE_TYPE_SWITCH, EMS_DEVICE_TYPE_CONTROLLER, EMS_DEVICE_TYPE_CONNECT, EMS_DEVICE_TYPE_UNKNOWN } _EMS_DEVICE_TYPE; +// to store mapping of device_ids to their string name +typedef struct { + _EMS_DEVICE_TYPE device_type; + char device_type_string[30]; +} _EMS_Device_Types; + +// mapping for EMS_Devices_Type +const _EMS_Device_Types EMS_Devices_Types[] = { + + {EMS_DEVICE_TYPE_UNKNOWN, EMS_MODELTYPE_UNKNOWN_STRING}, // the first, at index 0 is reserved for "unknown" + {EMS_DEVICE_TYPE_NONE, "All"}, + {EMS_DEVICE_TYPE_SERVICEKEY, "Me"}, + {EMS_DEVICE_TYPE_BOILER, "Boiler"}, + {EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"}, + {EMS_DEVICE_TYPE_MIXING, "Mixing Module"}, + {EMS_DEVICE_TYPE_SOLAR, "Solar Module"}, + {EMS_DEVICE_TYPE_HEATPUMP, "Heat Pump"}, + {EMS_DEVICE_TYPE_GATEWAY, "Gateway"}, + {EMS_DEVICE_TYPE_SWITCH, "Switching Module"}, + {EMS_DEVICE_TYPE_CONTROLLER, "Controller"}, + {EMS_DEVICE_TYPE_CONNECT, "Connect Module"} + +}; + // to store all known EMS devices to date typedef struct { uint8_t product_id; @@ -220,13 +257,6 @@ typedef struct { uint8_t flags; } _EMS_Device; -// to store mapping of device_ids to their string name -typedef struct { - uint8_t device_id; - _EMS_DEVICE_TYPE device_type; - char device_type_string[30]; -} _EMS_Device_Types; - // for storing all recognised EMS devices typedef struct { _EMS_DEVICE_TYPE device_type; // type (see above) @@ -248,18 +278,25 @@ typedef struct { uint8_t product_id; char version[10]; + uint8_t brand; // 0=unknown, 1=bosch, 2=junkers, 3=buderus, 4=nefit, 5=sieger, 11=worcester + // UBAParameterWW - uint8_t wWActivated; // Warm Water activated - uint8_t wWSelTemp; // Warm Water selected temperature - uint8_t wWCircPump; // Warm Water circulation pump Available - uint8_t wWDesiredTemp; // Warm Water desired temperature - uint8_t wWComfort; // Warm water comfort or ECO mode + uint8_t wWActivated; // Warm Water activated + uint8_t wWSelTemp; // Warm Water selected temperature + uint8_t wWCircPump; // Warm Water circulation pump Available + uint8_t wWCircPumpMode; // Warm Water circulation pump mode (1 = 1x3min, ..., 6=6x3min, 7 continuous) + uint8_t wWCircPumpType; // Warm Water circulation pump type (0 = charge pump, 0xff = 3-way pump) + uint8_t wWDesinfectTemp; // Warm Water desinfection temperature + uint8_t wWComfort; // Warm water comfort or ECO mode // UBAMonitorFast uint8_t selFlowTemp; // Selected flow temperature uint16_t curFlowTemp; // Current flow temperature + uint16_t wwStorageTemp1; // warm water storage temp 1 + uint16_t wwStorageTemp2; // warm water storage temp 2 uint16_t retTemp; // Return temperature uint8_t burnGas; // Gas on/off + uint8_t wWMode; // warm water mode uint8_t fanWork; // Fan on/off uint8_t ignWork; // Ignition on/off uint8_t heatPmp; // Circulating pump on/off @@ -275,6 +312,7 @@ typedef struct { // UBAMonitorSlow int16_t extTemp; // Outside temperature uint16_t boilTemp; // Boiler temperature + uint16_t exhaustTemp; // Exhaust temperature uint8_t pumpMod; // Pump modulation uint32_t burnStarts; // # burner starts uint32_t burnWorkMin; // Total burner operating time @@ -282,11 +320,17 @@ typedef struct { uint16_t switchTemp; // Switch temperature // UBAMonitorWWMessage - 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 - uint8_t wWCurFlow; // Warm Water current flow in l/min + uint8_t wWSetTmp; // set temp WW (DHW) + uint16_t wWCurTmp; // Warm Water current temperature + uint16_t wWCurTmp2; // Warm Water current temperature storage + uint32_t wWStarts; // Warm Water # starts + uint32_t wWWorkM; // Warm Water # minutes + uint8_t wWOneTime; // Warm Water one time function on/off + uint8_t wWDesinfecting; // Warm Water desinfection on/off + uint8_t wWReadiness; // Warm Water readiness on/off + uint8_t wWRecharging; // Warm Water recharge on/off + uint8_t wWTemperatureOK; // Warm Water temperature ok on/off + uint8_t wWCurFlow; // Warm Water current flow in l/min // UBATotalUptimeMessage uint32_t UBAuptime; // Total UBA working hours @@ -299,7 +343,6 @@ typedef struct { // calculated values uint8_t tapwaterActive; // Hot tap water is on/off uint8_t heatingActive; // Central heating is on/off - } _EMS_Boiler; /* @@ -322,34 +365,46 @@ typedef struct { uint16_t flowTemp; uint8_t pumpMod; uint8_t valveStatus; -} _EMS_Mixing_HC; + uint8_t flowSetTemp; +} _EMS_MixingModule_HC; -// Mixer data +// Mixing Module per WWC typedef struct { - uint8_t device_id; - uint8_t device_flags; - const char * device_desc_p; - uint8_t product_id; - char version[10]; - bool detected; - _EMS_Mixing_HC hc[EMS_THERMOSTAT_MAXHC]; // array for the 4 heating circuits -} _EMS_Mixing; + uint8_t wwc; // warm water circuit 1, 2 + bool active; // true if there is data for this WWC + uint16_t flowTemp; + uint8_t pumpMod; + uint8_t tempStatus; +} _EMS_MixingModule_WWC; -// Solar Module - SM10/SM100/ISM1 +// Mixing data +typedef struct { + uint8_t device_id; + uint8_t device_flags; + const char * device_desc_p; + uint8_t product_id; + char version[10]; + _EMS_MixingModule_HC hc[EMS_MIXING_MAXHC]; // array for the 4 heating circuits + _EMS_MixingModule_WWC wwc[EMS_MIXING_MAXWWC]; // array for the 2 ww circuits +} _EMS_MixingModule; + +// Solar Module - SM10/SM100/SM200/ISM1 typedef struct { uint8_t device_id; // the device ID of the Solar Module uint8_t device_flags; // Solar Module flags const char * device_desc_p; uint8_t product_id; char version[10]; - int16_t collectorTemp; // collector temp - int16_t bottomTemp; // bottom temp + int16_t collectorTemp; // collector temp (TS1) + int16_t bottomTemp; // bottom temp (TS2) + int16_t bottomTemp2; // bottom temp cylinder 2 (TS5) uint8_t pumpModulation; // modulation solar pump uint8_t pump; // pump active + uint8_t valveStatus; // valve status (VS2) int16_t setpoint_maxBottomTemp; // setpoint for maximum collector temp - uint16_t EnergyLastHour; - uint16_t EnergyToday; - uint16_t EnergyTotal; + uint32_t EnergyLastHour; + uint32_t EnergyToday; + uint32_t EnergyTotal; uint32_t pumpWorkMin; // Total solar pump operating time } _EMS_SolarModule; @@ -360,7 +415,7 @@ typedef struct { int16_t setpoint_roomTemp; // current set temp int16_t curr_roomTemp; // current room temp uint8_t mode; // 0=low, 1=manual, 2=auto (or night, day on RC35s) - uint8_t day_mode; // 0=night, 1=day + uint8_t mode_type; // 0=night/eco, 1=day/comfort uint8_t summer_mode; uint8_t holiday_mode; uint8_t daytemp; @@ -372,13 +427,22 @@ typedef struct { // Thermostat data typedef struct { - uint8_t device_id; // the device ID of the thermostat - uint8_t device_flags; // thermostat model flags - const char * device_desc_p; - uint8_t product_id; - char version[10]; - char datetime[25]; // HH:MM:SS DD/MM/YYYY - bool write_supported; + uint8_t device_id; // the device ID of the thermostat + uint8_t device_flags; // thermostat model flags + const char * device_desc_p; + uint8_t product_id; + char version[10]; + char datetime[25]; // HH:MM:SS DD/MM/YYYY + bool write_supported; + + // Installation parameters (tested on RC30) + uint8_t ibaMainDisplay; // 00, display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 9 smoke temp + uint8_t ibaLanguage; // 01, language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian + uint8_t ibaCalIntTemperature; // 02, offset int. temperature sensor, by * 0.1 Kelvin + int16_t ibaMinExtTemperature; // 05, min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 (actually a int8_t, coded as int16_t to benefit from negative value rendering) + uint8_t ibaBuildingType; // 06, building type: 0 = light, 1 = medium, 2 = heavy + uint8_t ibaClockOffset; // 12, offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s + _EMS_Thermostat_HC hc[EMS_THERMOSTAT_MAXHC]; // array for the 4 heating circuits } _EMS_Thermostat; @@ -393,49 +457,93 @@ typedef struct { EMS_processType_cb processType_cb; } _EMS_Type; +typedef enum : uint8_t { + EMS_THERMOSTAT_MODE_UNKNOWN = 0, + EMS_THERMOSTAT_MODE_OFF, + EMS_THERMOSTAT_MODE_MANUAL, + EMS_THERMOSTAT_MODE_AUTO, + EMS_THERMOSTAT_MODE_HEAT, // 'heizen' + EMS_THERMOSTAT_MODE_NIGHT, + EMS_THERMOSTAT_MODE_DAY, + EMS_THERMOSTAT_MODE_ECO, // 'sparen' + EMS_THERMOSTAT_MODE_COMFORT, + EMS_THERMOSTAT_MODE_HOLIDAY, + EMS_THERMOSTAT_MODE_NOFROST +} _EMS_THERMOSTAT_MODE; + +#define EMS_THERMOSTAT_MODE_UNKNOWN_STR "unknown" +#define EMS_THERMOSTAT_MODE_OFF_STR "off" +#define EMS_THERMOSTAT_MODE_MANUAL_STR "manual" +#define EMS_THERMOSTAT_MODE_AUTO_STR "auto" +#define EMS_THERMOSTAT_MODE_HEAT_STR "heat" +#define EMS_THERMOSTAT_MODE_NIGHT_STR "night" +#define EMS_THERMOSTAT_MODE_DAY_STR "day" +#define EMS_THERMOSTAT_MODE_ECO_STR "eco" +#define EMS_THERMOSTAT_MODE_COMFORT_STR "comfort" +#define EMS_THERMOSTAT_MODE_HOLIDAY_STR "holiday" +#define EMS_THERMOSTAT_MODE_NOFROST_STR "nofrost" + // function definitions -void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length); -void ems_parseTelegram(uint8_t * telegram, uint8_t len); -void ems_init(); -void ems_doReadCommand(uint16_t type, uint8_t dest); -void ems_sendRawTelegram(char * telegram); -void ems_scanDevices(); -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_setThermostatTemp(float temperature, uint8_t hc, uint8_t temptype = 0); -void ems_setThermostatMode(uint8_t mode, uint8_t hc); -void ems_setWarmWaterTemp(uint8_t temperature); -void ems_setFlowTemp(uint8_t temperature); -void ems_setWarmWaterActivated(bool activated); -void ems_setWarmWaterOnetime(bool activated); -void ems_setWarmTapWaterActivated(bool activated); -void ems_setPoll(bool b); -void ems_setLogging(_EMS_SYS_LOGGING loglevel, uint16_t type_id = 0); -void ems_setWarmWaterModeComfort(uint8_t comfort); -void ems_setModels(); -void ems_setTxDisabled(bool b); -void ems_setTxMode(uint8_t mode); -char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, bool name_only = false); -bool ems_getDeviceTypeDescription(uint8_t device_id, char * buffer); -void ems_getThermostatValues(); -void ems_getBoilerValues(); -void ems_getSolarModuleValues(); -bool ems_getPoll(); -bool ems_getTxEnabled(); -bool ems_getThermostatEnabled(); -bool ems_getMixingDeviceEnabled(); -bool ems_getBoilerEnabled(); -bool ems_getSolarModuleEnabled(); -bool ems_getHeatPumpEnabled(); +void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length); +void ems_parseTelegram(uint8_t * telegram, uint8_t len); +void ems_init(); +void ems_doReadCommand(uint16_t type, uint8_t dest, uint8_t offset = 0); +void ems_sendRawTelegram(char * telegram); +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_setSettingsLanguage(uint8_t lg); +void ems_setSettingsBuilding(uint8_t bg); +void ems_setSettingsDisplay(uint8_t ds); + +void ems_setThermostatTemp(float temperature, uint8_t hc, _EMS_THERMOSTAT_MODE temptype); +void ems_setThermostatTemp(float temperature, uint8_t hc, const char * mode_s); +void ems_setThermostatMode(_EMS_THERMOSTAT_MODE mode, uint8_t hc); +void ems_setThermostatMode(const char * mode_s, uint8_t hc); +void ems_setWarmWaterTemp(uint8_t temperature); +void ems_setFlowTemp(uint8_t temperature); +void ems_setWarmWaterActivated(bool activated); +void ems_setWarmWaterOnetime(bool activated); +void ems_setWarmWaterCirculation(bool activated); +void ems_setWarmTapWaterActivated(bool activated); +void ems_setPoll(bool b); +void ems_setLogging(_EMS_SYS_LOGGING loglevel, uint16_t type_id); +void ems_setLogging(_EMS_SYS_LOGGING loglevel, bool quiet = false); +void ems_setWarmWaterModeComfort(uint8_t comfort); +void ems_setModels(); +void ems_setTxDisabled(bool b); +void ems_setTxMode(uint8_t mode); +void ems_setEMSbusid(uint8_t id); +void ems_setMasterThermostat(uint8_t product_id); + +char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, bool name_only = false); +bool ems_getDeviceTypeDescription(uint8_t device_id, char * buffer); +char * ems_getDeviceTypeName(_EMS_DEVICE_TYPE device_type, char * buffer); +_EMS_THERMOSTAT_MODE ems_getThermostatMode(const char * mode_s); +void ems_getThermostatValues(); +void ems_getBoilerValues(); +void ems_getSettingsValues(); +void ems_getSolarModuleValues(); +void ems_getMixingModuleValues(); +char * ems_getThermostatModeString(_EMS_THERMOSTAT_MODE mode, char * mode_str); + +bool ems_getPoll(); +bool ems_getTxEnabled(); + +bool ems_getThermostatEnabled(); +bool ems_getMixingModuleEnabled(); +bool ems_getBoilerEnabled(); +bool ems_getSolarModuleEnabled(); +bool ems_getHeatPumpEnabled(); + bool ems_getBusConnected(); _EMS_SYS_LOGGING ems_getLogging(); -uint8_t ems_getThermostatModel(); -uint8_t ems_getSolarModuleModel(); +uint8_t ems_getThermostatFlags(); +uint8_t ems_getSolarModuleFlags(); void ems_discoverModels(); bool ems_getTxCapable(); uint32_t ems_getPollFrequency(); @@ -443,6 +551,8 @@ bool ems_getTxDisabled(); void ems_Device_add_flags(unsigned int flags); bool ems_Device_has_flags(unsigned int flags); void ems_Device_remove_flags(unsigned int flags); +bool ems_isHT3(); +void ems_scanDevices(); // private functions uint8_t _crcCalculator(uint8_t * data, uint8_t len); @@ -450,14 +560,15 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram); void _debugPrintPackage(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color); void _ems_clearTxData(); void _removeTxQueue(); -uint8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram); +int8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram); +void _printMessage(_EMS_RxTelegram * EMS_RxTelegram, const int show_type = -1); // 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_SolarModule EMS_SolarModule; -extern _EMS_HeatPump EMS_HeatPump; -extern _EMS_Mixing EMS_Mixing; +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_MixingModule EMS_MixingModule; extern std::list<_Detected_Device> Devices; diff --git a/src/ems_devices.h b/src/ems_devices.h index 3d09e6ec0..5f25aeffc 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -12,41 +12,7 @@ #include "ems.h" // Fixed EMS Device 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, SM100 and ISM1 -#define EMS_ID_HP 0x38 // HeatPump -#define EMS_ID_GATEWAY 0x48 // Gateway e.g. KM200 Web Gateway -#define EMS_ID_MIXING1 0x20 // Mixing -#define EMS_ID_MIXING2 0x21 // Mixing -#define EMS_ID_SWITCH 0x11 // Switch -#define EMS_ID_CONTROLLER 0x09 // Controller -#define EMS_ID_CONNECT1 0x02 // Connect -#define EMS_ID_CONNECT2 0x50 // Connect -#define EMS_ID_THERMOSTAT1 0x10 // Thermostat -#define EMS_ID_THERMOSTAT2 0x17 // Thermostat -#define EMS_ID_THERMOSTAT3 0x18 // Thermostat - -// mapping for EMS_Devices_Type -const _EMS_Device_Types EMS_Devices_Types[] = { - - {EMS_ID_BOILER, EMS_DEVICE_TYPE_BOILER, "UBAMaster"}, - {EMS_ID_THERMOSTAT1, EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"}, - {EMS_ID_THERMOSTAT2, EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"}, - {EMS_ID_THERMOSTAT3, EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"}, - {EMS_ID_SM, EMS_DEVICE_TYPE_SOLAR, "Solar Module"}, - {EMS_ID_HP, EMS_DEVICE_TYPE_HEATPUMP, "Heat Pump"}, - {EMS_ID_GATEWAY, EMS_DEVICE_TYPE_GATEWAY, "Gateway"}, - {EMS_ID_ME, EMS_DEVICE_TYPE_SERVICEKEY, "Me"}, - {EMS_ID_NONE, EMS_DEVICE_TYPE_NONE, "All"}, - {EMS_ID_MIXING1, EMS_DEVICE_TYPE_MIXING, "Mixing Module"}, - {EMS_ID_MIXING2, EMS_DEVICE_TYPE_MIXING, "Mixing Module"}, - {EMS_ID_SWITCH, EMS_DEVICE_TYPE_SWITCH, "Switching Module"}, - {EMS_ID_CONTROLLER, EMS_DEVICE_TYPE_CONTROLLER, "Controller"}, - {EMS_ID_CONNECT1, EMS_DEVICE_TYPE_CONNECT, "Connect"}, - {EMS_ID_CONNECT2, EMS_DEVICE_TYPE_CONNECT, "Connect"} - -}; +#define EMS_ID_BOILER 0x08 // all UBA Boilers have 0x08 /* * Common Type @@ -61,6 +27,7 @@ const _EMS_Device_Types EMS_Devices_Types[] = { #define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast #define EMS_TYPE_UBAMonitorWWMessage 0x34 // is an automatic monitor broadcast #define EMS_TYPE_UBAMaintenanceStatusMessage 0x1C // is an automatic monitor broadcast +#define EMS_TYPE_MC10Status 0x2A #define EMS_TYPE_UBAParameterWW 0x33 #define EMS_TYPE_UBATotalUptimeMessage 0x14 #define EMS_TYPE_UBAFlags 0x35 @@ -77,6 +44,7 @@ const _EMS_Device_Types EMS_Devices_Types[] = { #define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature #define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated #define EMS_OFFSET_UBAParameterWW_wwOneTime 0x00 // WW OneTime loading +#define EMS_OFFSET_UBAParameterWW_wwCirulation 1 // WW circulation #define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode #define EMS_VALUE_UBAParameterWW_wwComfort_Hot 0x00 // the value for hot #define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco @@ -84,19 +52,73 @@ const _EMS_Device_Types EMS_Devices_Types[] = { #define EMS_OFFSET_UBASetPoints_flowtemp 0 // flow temp -// SM and HP Types +// Installation settings +#define EMS_TYPE_IBASettingsMessage 0xA5 // installation settings +#define EMS_OFFSET_IBASettings_Display 0 // display +#define EMS_OFFSET_IBASettings_Language 1 // language +#define EMS_OFFSET_IBASettings_MinExtTemp 5 // min. ext. temperature +#define EMS_OFFSET_IBASettings_Building 6 // building +#define EMS_OFFSET_IBASettings_CalIntTemp 2 // cal. int. temperature +#define EMS_OFFSET_IBASettings_ClockOffset 12 // clock offset + +#define EMS_VALUE_IBASettings_LANG_GERMAN 0 +#define EMS_VALUE_IBASettings_LANG_DUTCH 1 +#define EMS_VALUE_IBASettings_LANG_FRENCH 2 +#define EMS_VALUE_IBASettings_LANG_ITALIAN 3 + +#define EMS_VALUE_IBASettings_BUILDING_LIGHT 0 +#define EMS_VALUE_IBASettings_BUILDING_MEDIUM 1 +#define EMS_VALUE_IBASettings_BUILDING_HEAVY 2 + +#define EMS_VALUE_IBASettings_DISPLAY_INTTEMP 0 +#define EMS_VALUE_IBASettings_DISPLAY_INTSETPOINT 1 +#define EMS_VALUE_IBASettings_DISPLAY_EXTTEMP 2 +#define EMS_VALUE_IBASettings_DISPLAY_BURNERTEMP 3 +#define EMS_VALUE_IBASettings_DISPLAY_WWTEMP 4 +#define EMS_VALUE_IBASettings_DISPLAY_FUNCMODE 5 +#define EMS_VALUE_IBASettings_DISPLAY_TIME 6 +#define EMS_VALUE_IBASettings_DISPLAY_DATE 7 +#define EMS_VALUE_IBASettings_DISPLAY_SMOKETEMP 9 + +// Mixing Modules +// MM100/MM200 (EMS Plus) +#define EMS_TYPE_MMPLUSStatusMessage_HC1 0x01D7 // mixing status HC1 +#define EMS_TYPE_MMPLUSStatusMessage_HC2 0x01D8 // mixing status HC2 +#define EMS_TYPE_MMPLUSStatusMessage_HC3 0x01D9 // mixing status HC3 +#define EMS_TYPE_MMPLUSStatusMessage_HC4 0x01DA // mixing status HC4 +#define EMS_TYPE_MMPLUSStatusMessage_WWC1 0x0231 // mixing status WWC1 +#define EMS_TYPE_MMPLUSStatusMessage_WWC2 0x0232 // mixing status WWC2 +#define EMS_OFFSET_MMPLUSStatusMessage_flow_temp 3 // flow temperature +#define EMS_OFFSET_MMPLUSStatusMessage_pump_mod 5 // pump modulation +#define EMS_OFFSET_MMPLUSStatusMessage_valve_status 2 // valve in percent +#define EMS_OFFSET_MMPLUSStatusMessage_WW_flow_temp 0 // flow temperature +#define EMS_OFFSET_MMPLUSStatusMessage_WW_pump_mod 2 // pump on 6, off 0 +#define EMS_OFFSET_MMPLUSStatusMessage_WW_temp_status 11 // 0,1,2 + +// MM10 +#define EMS_TYPE_MMStatusMessage 0xAB // mixing status +#define EMS_OFFSET_MMStatusMessage_flow_set 0 // flow setpoint +#define EMS_OFFSET_MMStatusMessage_flow_temp 1 // flow temperature +#define EMS_OFFSET_MMStatusMessage_pump_mod 3 // pump modulation in percent +#define EMS_OFFSET_MMStatusMessage_valve_status 4 // valve 0..255 +#define EMS_TYPE_MM10ParameterMessage 0xAC // mixing parameters + +// Solar Module +// Assuming here that the SM200 behaves like SM100 #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 - +// ISPM solar module #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) +// Heat Pump +#define EMS_TYPE_HPMonitor1 0xE3 // HeatPump Monitor 1 +#define EMS_TYPE_HPMonitor2 0xE5 // HeatPump Monitor 2 + /* * Thermostat Types */ @@ -118,6 +140,16 @@ const _EMS_Device_Types EMS_Devices_Types[] = { #define EMS_OFFSET_RC20StatusMessage_setpoint 1 // setpoint temp #define EMS_OFFSET_RC20StatusMessage_curr 2 // current temp +#define EMS_TYPE_RC20NStatusMessage 0xAE +#define EMS_OFFSET_RC20NStatusMessage_setpoint 2 // setpoint temp in AE +#define EMS_OFFSET_RC20NStatusMessage_curr 3 // current temp in AE + +#define EMS_TYPE_RC20NSet 0xAD +#define EMS_OFFSET_RC20NSet_temp_day 2 // position of thermostat setpoint temperature for day time +#define EMS_OFFSET_RC20NSet_temp_night 1 // position of thermostat setpoint temperature for night time +#define EMS_OFFSET_RC20NSet_mode 3 // position mode +#define EMS_OFFSET_RC20NSet_heatingtype 0 + #define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode #define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode #define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature @@ -142,7 +174,6 @@ const _EMS_Device_Types EMS_Devices_Types[] = { #define EMS_OFFSET_RC35StatusMessage_mode 1 // day mode, also summer on RC3's #define EMS_OFFSET_RC35StatusMessage_mode1 0 // for holiday mode - #define EMS_TYPE_RC35Set_HC1 0x3D // for setting values like temp and mode (Working mode HC1) #define EMS_TYPE_RC35Set_HC2 0x47 // for setting values like temp and mode (Working mode HC2) #define EMS_TYPE_RC35Set_HC3 0x51 // for setting values like temp and mode (Working mode HC3) @@ -151,9 +182,10 @@ const _EMS_Device_Types EMS_Devices_Types[] = { #define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode #define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time #define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time -#define EMS_OFFSET_RC35Set_temp_holiday 3 // temp during holiday 0x47 -#define EMS_OFFSET_RC35Set_heatingtype 0 // floor heating = 3 0x47 -#define EMS_OFFSET_RC35Set_circuitcalctemp 14 // calculated circuit temperature 0x48 +#define EMS_OFFSET_RC35Set_temp_holiday 3 // temp during holiday mode +#define EMS_OFFSET_RC35Set_heatingtype 0 // e.g. floor heating = 3 +#define EMS_OFFSET_RC35Set_circuitcalctemp 14 // calculated circuit temperature +#define EMS_OFFSET_RC35Set_seltemp 37 // selected temp // Easy specific #define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat @@ -180,21 +212,38 @@ const _EMS_Device_Types EMS_Devices_Types[] = { #define EMS_OFFSET_RCPLUSSet_temp_setpoint 8 // temp setpoint, when changing of templevel (in auto) value is reset and set to FF #define EMS_OFFSET_RCPLUSSet_manual_setpoint 10 // manual setpoint -// Junkers FR10, FR50, FW100 (EMS Plus) -#define EMS_TYPE_JunkersStatusMessage 0x6F // is an automatic thermostat broadcast giving us temps +// Junkers FR10, FR50, FW100, FW120 (EMS Plus) +// HC1 = 0x6F-0x72 +#define EMS_TYPE_JunkersStatusMessage_HC1 0x6F +#define EMS_TYPE_JunkersStatusMessage_HC2 0x70 +#define EMS_TYPE_JunkersStatusMessage_HC3 0x71 +#define EMS_TYPE_JunkersStatusMessage_HC4 0x72 + #define EMS_OFFSET_JunkersStatusMessage_daymode 0 // 3 = day, 2 = night #define EMS_OFFSET_JunkersStatusMessage_mode 1 // current mode, 1 = manual, 2 = auto #define EMS_OFFSET_JunkersStatusMessage_setpoint 2 // setpoint temp #define EMS_OFFSET_JunkersStatusMessage_curr 4 // current temp -// MM100 (EMS Plus) -#define EMS_TYPE_MMPLUSStatusMessage_HC1 0x01D7 // mixer status HC1 -#define EMS_TYPE_MMPLUSStatusMessage_HC2 0x01D8 // mixer status HC2 -#define EMS_TYPE_MMPLUSStatusMessage_HC3 0x01D9 // mixer status HC3 -#define EMS_TYPE_MMPLUSStatusMessage_HC4 0x01DA // mixer status HC4 -#define EMS_OFFSET_MMPLUSStatusMessage_flow_temp 3 // flow temperature -#define EMS_OFFSET_MMPLUSStatusMessage_pump_mod 5 // pump modulation -#define EMS_OFFSET_MMPLUSStatusMessage_valve_status 2 // valve in percent +// HC1-4 0x65-0x68 - EMS_DEVICE_FLAG_JUNKERS1 +// Junkers FR10, FR50, FW100, FW120 +#define EMS_TYPE_JunkersSetMessage1_HC1 0x65 +#define EMS_TYPE_JunkersSetMessage1_HC2 0x66 +#define EMS_TYPE_JunkersSetMessage1_HC3 0x67 +#define EMS_TYPE_JunkersSetMessage1_HC4 0x68 +#define EMS_OFFSET_JunkersSetMessage_day_temp 0x11 // EMS offset to set temperature on thermostat for day mode +#define EMS_OFFSET_JunkersSetMessage_night_temp 0x10 // EMS offset to set temperature on thermostat for night mode +#define EMS_OFFSET_JunkersSetMessage_no_frost_temp 0x0F // EMS offset to set temperature on thermostat for no frost mode +#define EMS_OFFSET_JunkersSetMessage_set_mode 0x0E // EMS offset to set mode on thermostat + +// HC1-4 0x79-0x7C - EMS_DEVICE_FLAG_JUNKERS2 +// Junkers FR100 +#define EMS_TYPE_JunkersSetMessage2_HC1 0x79 +#define EMS_TYPE_JunkersSetMessage2_HC2 0x7A +#define EMS_TYPE_JunkersSetMessage2_HC3 0x7B +#define EMS_TYPE_JunkersSetMessage2_HC4 0x7C +#define EMS_OFFSET_JunkersSetMessage2_no_frost_temp 0x05 +#define EMS_OFFSET_JunkersSetMessage2_eco_temp 0x06 +#define EMS_OFFSET_JunkersSetMessage3_heat 0x07 /* * Table of all known EMS Devices @@ -203,7 +252,7 @@ const _EMS_Device_Types EMS_Devices_Types[] = { static const _EMS_Device EMS_Devices[] = { // - // UBA Masters - typically with device_id of 0x08 + // UBA Masters - must have device_id of 0x08 // {72, EMS_DEVICE_TYPE_BOILER, "MC10 Module", EMS_DEVICE_FLAG_NONE}, {123, EMS_DEVICE_TYPE_BOILER, "Buderus GBx72/Nefit Trendline/Junkers Cerapur/Worcester Greenstar Si/27i", EMS_DEVICE_FLAG_NONE}, @@ -213,7 +262,8 @@ static const _EMS_Device EMS_Devices[] = { {208, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax plus/GB192/Bosch Condens GC9000", EMS_DEVICE_FLAG_NONE}, {64, EMS_DEVICE_TYPE_BOILER, "Sieger BK13,BK15/Nefit Smartline/Buderus GB1x2", EMS_DEVICE_FLAG_NONE}, {234, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax Plus GB122", EMS_DEVICE_FLAG_NONE}, - {95, EMS_DEVICE_TYPE_BOILER, "Bosch Condens 2500/Buderus Logamax GB062/Junkers Cerapur Top/Worcester Greenstar i/Generic HT3", EMS_DEVICE_FLAG_NONE}, + {84, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax Plus GB022", EMS_DEVICE_FLAG_NONE}, + {95, EMS_DEVICE_TYPE_BOILER, "Bosch Condens 2500/Buderus Logamax,Logomatic/Junkers Cerapur Top/Worcester Greenstar i/Generic HT3", EMS_DEVICE_FLAG_NONE}, {122, EMS_DEVICE_TYPE_BOILER, "Nefit Proline", EMS_DEVICE_FLAG_NONE}, {170, EMS_DEVICE_TYPE_BOILER, "Buderus Logano GB212", EMS_DEVICE_FLAG_NONE}, {172, EMS_DEVICE_TYPE_BOILER, "Nefit Enviline", EMS_DEVICE_FLAG_NONE}, @@ -223,19 +273,17 @@ static const _EMS_Device EMS_Devices[] = { // {73, EMS_DEVICE_TYPE_SOLAR, "SM10 Solar Module", EMS_DEVICE_FLAG_SM10}, {163, EMS_DEVICE_TYPE_SOLAR, "SM100 Solar Module", EMS_DEVICE_FLAG_SM100}, + {164, EMS_DEVICE_TYPE_SOLAR, "SM200 Solar Module", EMS_DEVICE_FLAG_SM100}, {101, EMS_DEVICE_TYPE_SOLAR, "Junkers ISM1 Solar Module", EMS_DEVICE_FLAG_SM100}, {162, EMS_DEVICE_TYPE_SOLAR, "SM50 Solar Module", EMS_DEVICE_FLAG_SM100}, // // Mixing Devices - type 0x20 or 0x21 // - {160, EMS_DEVICE_TYPE_MIXING, "MM100 Mixing Module", EMS_DEVICE_FLAG_NONE}, - {161, EMS_DEVICE_TYPE_MIXING, "MM200 Mixing Module", EMS_DEVICE_FLAG_NONE}, - {69, EMS_DEVICE_TYPE_MIXING, "MM10 Mixer Module", EMS_DEVICE_FLAG_NONE}, - {159, EMS_DEVICE_TYPE_MIXING, "MM50 Mixing Module", EMS_DEVICE_FLAG_NONE}, - {79, EMS_DEVICE_TYPE_MIXING, "MM100 Mixer Module", EMS_DEVICE_FLAG_NONE}, - {80, EMS_DEVICE_TYPE_MIXING, "MM200 Mixer Module", EMS_DEVICE_FLAG_NONE}, - {78, EMS_DEVICE_TYPE_MIXING, "MM400 Mixer Module", EMS_DEVICE_FLAG_NONE}, + {160, EMS_DEVICE_TYPE_MIXING, "MM100 Mixing Module", EMS_DEVICE_FLAG_MMPLUS}, + {161, EMS_DEVICE_TYPE_MIXING, "MM200 Mixing Module", EMS_DEVICE_FLAG_MMPLUS}, + {69, EMS_DEVICE_TYPE_MIXING, "MM10 Mixing Module", EMS_DEVICE_FLAG_MM10}, + {159, EMS_DEVICE_TYPE_MIXING, "MM50 Mixing Module", EMS_DEVICE_FLAG_MMPLUS}, // // HeatPump - type 0x38 @@ -244,25 +292,29 @@ static const _EMS_Device EMS_Devices[] = { {200, EMS_DEVICE_TYPE_HEATPUMP, "HeatPump Module", EMS_DEVICE_FLAG_NONE}, // - // Other devices, like 0x11 for Switching, 0x09 for controllers, 0x02 for Connect, 0x48 for Gateway + // Other devices like controllers and modems + // such as 0x11 for Switching, 0x09 for controllers, 0x02 for Connect, 0x48 for Gateway // - {71, EMS_DEVICE_TYPE_SWITCH, "WM10 Switch Module", EMS_DEVICE_FLAG_NONE}, // 0x11 - {68, EMS_DEVICE_TYPE_CONTROLLER, "BC10/RFM20 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x09 - {218, EMS_DEVICE_TYPE_CONTROLLER, "Junkers M200/Buderus RFM200 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x50 - {190, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 - {114, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 - {125, EMS_DEVICE_TYPE_CONTROLLER, "BC25 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 - {169, EMS_DEVICE_TYPE_CONTROLLER, "BC40 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 - {152, EMS_DEVICE_TYPE_CONTROLLER, "Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 - {95, EMS_DEVICE_TYPE_CONTROLLER, "HT3 Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 - {230, EMS_DEVICE_TYPE_CONTROLLER, "BC Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 - {205, EMS_DEVICE_TYPE_CONNECT, "Nefit Moduline Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02 - {206, EMS_DEVICE_TYPE_CONNECT, "Bosch Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02 - {171, EMS_DEVICE_TYPE_CONNECT, "EMS-OT OpenTherm converter", EMS_DEVICE_FLAG_NONE}, // 0x02 - {189, EMS_DEVICE_TYPE_GATEWAY, "Web Gateway KM200", EMS_DEVICE_FLAG_NONE}, // 0x48 + {71, EMS_DEVICE_TYPE_SWITCH, "WM10 Switch Module", EMS_DEVICE_FLAG_NONE}, // 0x11 + {68, EMS_DEVICE_TYPE_CONTROLLER, "BC10/RFM20 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x09 + {218, EMS_DEVICE_TYPE_CONTROLLER, "Junkers M200/Buderus RFM200 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x50 + {190, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {114, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {125, EMS_DEVICE_TYPE_CONTROLLER, "BC25 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {169, EMS_DEVICE_TYPE_CONTROLLER, "BC40 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {152, EMS_DEVICE_TYPE_CONTROLLER, "Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {95, EMS_DEVICE_TYPE_CONTROLLER, "HT3 Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {209, EMS_DEVICE_TYPE_CONTROLLER, "W-B ErP Boiler Control Panel", EMS_DEVICE_FLAG_NONE}, // 0x09 + {230, EMS_DEVICE_TYPE_CONTROLLER, "BC Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09 + {205, EMS_DEVICE_TYPE_CONNECT, "Nefit Moduline Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02 + {206, EMS_DEVICE_TYPE_CONNECT, "Bosch Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02 + {171, EMS_DEVICE_TYPE_CONNECT, "EMS-OT OpenTherm converter", EMS_DEVICE_FLAG_NONE}, // 0x02 + {189, EMS_DEVICE_TYPE_GATEWAY, "Web Gateway KM200", EMS_DEVICE_FLAG_NONE}, // 0x48 + {94, EMS_DEVICE_TYPE_GATEWAY, "RC Remote Device", EMS_DEVICE_FLAG_NONE}, // 0x18 + {207, EMS_DEVICE_TYPE_CONTROLLER, "Worcester Sense II/Bosch CS200 Solar Controller", EMS_DEVICE_FLAG_NONE}, // 0x10 // - // Thermostats, typically device id of 0x10, 0x17 and 0x18 + // Thermostats, typically device id of 0x10, 0x17, 0x18, 0x38 (RC100), 0x39 (Easy) // // Easy devices - not currently supporting write operations @@ -270,28 +322,30 @@ static const _EMS_Device EMS_Devices[] = { {203, EMS_DEVICE_TYPE_THERMOSTAT, "Bosch EasyControl CT200", EMS_DEVICE_FLAG_EASY | EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write {157, EMS_DEVICE_TYPE_THERMOSTAT, "Buderus RC200/Bosch CW100/Junkers CW100", EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write - // Buderus/Nefit + // Buderus/Nefit specific {79, EMS_DEVICE_TYPE_THERMOSTAT, "RC10/Moduline 100", EMS_DEVICE_FLAG_RC10}, // 0x17 + {80, EMS_DEVICE_TYPE_THERMOSTAT, "Moduline 200", EMS_DEVICE_FLAG_RC10}, // 0x17 {77, EMS_DEVICE_TYPE_THERMOSTAT, "RC20/Moduline 300", EMS_DEVICE_FLAG_RC20}, // 0x17 - {93, EMS_DEVICE_TYPE_THERMOSTAT, "RC20RF", EMS_DEVICE_FLAG_RC20}, // 0x18 - {67, EMS_DEVICE_TYPE_THERMOSTAT, "RC30", EMS_DEVICE_FLAG_RC30}, // 0x10 - {78, EMS_DEVICE_TYPE_THERMOSTAT, "RC30/Moduline 400", EMS_DEVICE_FLAG_RC30}, // 0x10 + {67, EMS_DEVICE_TYPE_THERMOSTAT, "RC30", EMS_DEVICE_FLAG_RC30N}, // 0x10 - based on RC35 + {78, EMS_DEVICE_TYPE_THERMOSTAT, "Moduline 400", EMS_DEVICE_FLAG_RC30}, // 0x10 {86, EMS_DEVICE_TYPE_THERMOSTAT, "RC35", EMS_DEVICE_FLAG_RC35}, // 0x10 + {93, EMS_DEVICE_TYPE_THERMOSTAT, "RC20RF", EMS_DEVICE_FLAG_RC20}, // 0x19 {158, EMS_DEVICE_TYPE_THERMOSTAT, "RC300/RC310/Moduline 3000/Bosch CW400/W-B Sense II", EMS_DEVICE_FLAG_RC300}, // 0x10 - {165, EMS_DEVICE_TYPE_THERMOSTAT, "RC100/Moduline 1010", EMS_DEVICE_FLAG_RC300 | EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write + {165, EMS_DEVICE_TYPE_THERMOSTAT, "RC100/Moduline 1000/1010", EMS_DEVICE_FLAG_RC100}, // 0x18, 0x38 // Sieger - {076, EMS_DEVICE_TYPE_THERMOSTAT, "Sieger ES73", EMS_DEVICE_FLAG_RC35}, // 0x10 + {76, EMS_DEVICE_TYPE_THERMOSTAT, "Sieger ES73", EMS_DEVICE_FLAG_RC35}, // 0x10 + {113, EMS_DEVICE_TYPE_THERMOSTAT, "Sieger ES72/Buderus RC20", EMS_DEVICE_FLAG_RC20N}, // 0x17 - // Junkers - {105, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW100", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write - {106, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW200", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write - {107, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR100", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write - {108, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR110", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write - {111, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR10", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write - {191, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR120", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write - {192, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW120", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE}, // 0x10, cannot write - {147, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR50", EMS_DEVICE_FLAG_JUNKERS | EMS_DEVICE_FLAG_NO_WRITE} // 0x10, cannot write + // Junkers - all 0x10 + {105, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW100", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10 + {106, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW200", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10 + {107, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR100", EMS_DEVICE_FLAG_JUNKERS2}, // 0x10 + {108, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR110", EMS_DEVICE_FLAG_JUNKERS2}, // 0x10 + {111, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR10", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10 + {147, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR50", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10 + {191, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR120", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10 + {192, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW120", EMS_DEVICE_FLAG_JUNKERS1} // 0x10 }; diff --git a/src/ems_utils.cpp b/src/ems_utils.cpp index 4d61cdfef..a96811115 100644 --- a/src/ems_utils.cpp +++ b/src/ems_utils.cpp @@ -76,11 +76,11 @@ char * _short_to_char(char * s, int16_t value, uint8_t decimals) { return s; } -// convert short (two bytes) to text string and prints it +// convert unsigned short (two bytes) to text string and prints it // 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) { // remove errors or invalid values - if (value == EMS_VALUE_USHORT_NOTSET) { + if (value == EMS_VALUE_USHORT_NOTSET) { // 0x7D00 strlcpy(s, "?", 10); return (s); } @@ -149,6 +149,7 @@ void _renderUShortValue(const char * prefix, const char * postfix, uint16_t valu // convert int (single byte) to text value and returns it char * _int_to_char(char * s, uint8_t value, uint8_t div) { + s[0] = '\0'; // reset if (value == EMS_VALUE_INT_NOTSET) { strlcpy(s, "?", sizeof(s)); return (s); @@ -200,7 +201,7 @@ void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, u } // takes a long value at prints it to debug log and prints -void _renderLongValue(const char * prefix, const char * postfix, uint32_t value) { +void _renderLongValue(const char * prefix, const char * postfix, uint32_t value, uint8_t div) { static char buffer[200] = {0}; strlcpy(buffer, " ", sizeof(buffer)); strlcat(buffer, prefix, sizeof(buffer)); @@ -210,7 +211,13 @@ void _renderLongValue(const char * prefix, const char * postfix, uint32_t value) strlcat(buffer, "?", sizeof(buffer)); } else { char s[20] = {0}; - strlcat(buffer, ltoa(value, s, 10), sizeof(buffer)); + if (div == 0) { + strlcat(buffer, ltoa(value, s, 10), sizeof(buffer)); + } else { + strlcat(buffer, ltoa(value / 10, s, 10), sizeof(buffer)); + strlcat(buffer, ".", sizeof(buffer)); + strlcat(buffer, ltoa(value % 10, s, 10), sizeof(buffer)); + } } if (postfix != nullptr) { diff --git a/src/ems_utils.h b/src/ems_utils.h index c3c624f63..93fe5a52f 100644 --- a/src/ems_utils.h +++ b/src/ems_utils.h @@ -22,7 +22,7 @@ void _renderShortValue(const char * prefix, const char * postfix, int16_t va void _renderUShortValue(const char * prefix, const char * postfix, uint16_t value, uint8_t decimals = 1); char * _int_to_char(char * s, uint8_t value, uint8_t div = 1); void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div = 1); -void _renderLongValue(const char * prefix, const char * postfix, uint32_t value); +void _renderLongValue(const char * prefix, const char * postfix, uint32_t value, uint8_t div = 0); void _renderBoolValue(const char * prefix, uint8_t value); char * _hextoa(uint8_t value, char * buffer); char * _smallitoa(uint8_t value, char * buffer); diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 923583298..c99fee1eb 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -224,27 +224,27 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) { } emsuart_tx_brk(); // send } else if (EMS_Sys_Status.emsTxMode == EMS_TXMODE_DEFAULT) { - /* - * 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 - * - */ + /* + * 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 + * + */ uint16_t wdc = EMS_TX_TO_COUNT; ETS_UART_INTR_DISABLE(); // disable rx interrupt diff --git a/src/my_config.h b/src/my_config.h index a36662d9d..0dc1ab297 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -10,8 +10,9 @@ #include "ems.h" -// TOPICS with _CMD_ are used for receiving commands from an MQTT Broker +// TOPICS with *_CMD_* are used for receiving commands from an MQTT Broker // EMS-ESP will subscribe to these topics + #define TOPIC_GENERIC_CMD "generic_cmd" // for receiving generic system commands via MQTT // MQTT for thermostat @@ -26,38 +27,57 @@ #define TOPIC_THERMOSTAT_CMD_DAYTEMP "daytemp" // day temp (RC35 specific) #define TOPIC_THERMOSTAT_CMD_NIGHTTEMP "nighttemp" // night temp (RC35 specific) #define TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP "holidayttemp" // holiday temp (RC35 specific) +#define TOPIC_THERMOSTAT_CMD_NOFROSTTEMP "nofrosttemp" // frost temp (Junkers specific) +#define TOPIC_THERMOSTAT_CMD_ECOTEMP "ecotemp" // eco temp (Junkers specific) +#define TOPIC_THERMOSTAT_CMD_HEATTEMP "heattemp" // heat temp (Junkers specific) #define THERMOSTAT_CURRTEMP "currtemp" // current temperature #define THERMOSTAT_SELTEMP "seltemp" // selected temperature #define THERMOSTAT_HC "hc" // which heating circuit number #define THERMOSTAT_MODE "mode" // mode +#define THERMOSTAT_MODETYPE "modetype" // mode type #define THERMOSTAT_DAYTEMP "daytemp" // RC35 specific #define THERMOSTAT_NIGHTTEMP "nighttemp" // RC35 specific #define THERMOSTAT_HOLIDAYTEMP "holidayttemp" // RC35 specific #define THERMOSTAT_HEATINGTYPE "heatingtype" // RC35 specific (3=floorheating) #define THERMOSTAT_CIRCUITCALCTEMP "circuitcalctemp" // RC35 specific +// mixing module +#define MIXING_HC "hc" // which heating circuit number +#define MIXING_WWC "wwc" // which warm water circuit number + // 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_CMD "boiler_cmd" // for receiving boiler commands via MQTT -#define TOPIC_BOILER_CMD_WWACTIVATED "boiler_cmd_wwactivated" // change water on/off -#define TOPIC_BOILER_CMD_WWONETIME "boiler_cmd_wwonetime" // warm warter one time loading -#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // wwtemp changes via MQTT -#define TOPIC_BOILER_CMD_COMFORT "comfort" // ww comfort setting via MQTT -#define TOPIC_BOILER_CMD_FLOWTEMP "flowtemp" // flowtemp value via MQTT +#define TOPIC_BOILER_CMD "boiler_cmd" // for receiving boiler commands via MQTT +#define TOPIC_BOILER_CMD_WWACTIVATED "boiler_cmd_wwactivated" // change water on/off +#define TOPIC_BOILER_CMD_WWONETIME "boiler_cmd_wwonetime" // warm warter one time loading +#define TOPIC_BOILER_CMD_WWCIRCULATION "boiler_cmd_wwcirculation" // start warm warter circulation +#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // wwtemp changes via MQTT +#define TOPIC_BOILER_CMD_COMFORT "comfort" // ww comfort setting via MQTT +#define TOPIC_BOILER_CMD_FLOWTEMP "flowtemp" // flowtemp value via MQTT + +// MQTT for settings +#define TOPIC_SETTINGS_DATA "settings_data" // for sending settings values to MQTT +#define TOPIC_SETTINGS_CMD "settings_cmd" // for receiving settings commands via MQTT +#define TOPIC_SETTINGS_CMD_DISPLAY "display" // change display +#define TOPIC_SETTINGS_CMD_LANGUAGE "language" // change language +#define TOPIC_SETTINGS_CMD_BUILDING "building" // change building +#define TOPIC_SETTINGS_CMD_MINEXTTEMP "minextTemp" // change min. ext. temp. // MQTT for mixing device #define TOPIC_MIXING_DATA "mixing_data" // for sending mixing device values to MQTT -// MQTT for SM10/SM100 Solar Module +// MQTT for SM10/SM100/SM200 Solar Module #define TOPIC_SM_DATA "sm_data" // topic name #define SM_COLLECTORTEMP "collectortemp" // collector temp -#define SM_BOTTOMTEMP "bottomtemp" // bottom temp +#define SM_BOTTOMTEMP "bottomtemp" // bottom temp1 +#define SM_BOTTOMTEMP2 "bottomtemp2" // bottom temp2 #define SM_PUMPMODULATION "pumpmodulation" // pump modulation #define SM_PUMP "pump" // pump active +#define SM_VALVESTATUS "valvestatus" // valve status #define SM_ENERGYLASTHOUR "energylasthour" // energy last hour #define SM_ENERGYTODAY "energytoday" // energy today #define SM_ENERGYTOTAL "energytotal" // energy total @@ -76,5 +96,7 @@ #define TOPIC_SHOWER_DURATION "duration" // duration of the last shower // MQTT for External Sensors -#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT -#define PAYLOAD_EXTERNAL_SENSORS "temp_%d" // for formatting the payload for each external dallas sensor +#define TOPIC_EXTERNAL_SENSORS "sensors" // topic for sending sensor values to MQTT +#define PAYLOAD_EXTERNAL_SENSOR_NUM "sensor" // which sensor # +#define PAYLOAD_EXTERNAL_SENSOR_ID "id" +#define PAYLOAD_EXTERNAL_SENSOR_TEMP "temp" diff --git a/src/version.h b/src/version.h index 7e3e390c1..db6dec5cf 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define APP_VERSION "1.9.4" +#define APP_VERSION "1.9.5" diff --git a/src/websrc/myesp.htm b/src/websrc/myesp.htm index 2ada3ff59..dd368199f 100644 --- a/src/websrc/myesp.htm +++ b/src/websrc/myesp.htm @@ -181,9 +181,9 @@
+ data-content="MQTT Server port number (default 1883)"> -
@@ -191,9 +191,9 @@
+ data-content="MQTT QOS 0, 1 or 2 (default 0)"> -
@@ -201,9 +201,9 @@
+ data-content="MQTT Keep Alive time in seconds (default 60)"> -
@@ -246,14 +246,14 @@ aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right" data-content="MQTT topic prefix (<mqtt base>/<host name>/)"> -
+ data-content="Enable or Disable an automatic MQTT topic publish with system status">
diff --git a/src/websrc/myesp.js b/src/websrc/myesp.js index 04a07161a..f3daa8b54 100644 --- a/src/websrc/myesp.js +++ b/src/websrc/myesp.js @@ -391,40 +391,6 @@ function listStats() { document.getElementById("mqttheartbeat").className = "label label-primary"; } - document.getElementById("mqttloghdr").setAttribute('data-content', "Topics are prefixed with " + ajaxobj.mqttloghdr); - - var mtable = document.getElementById("mqttlog"); - var obj = ajaxobj.mqttlog; - var tr, td; - - for (var i = 0; i < obj.length; i++) { - tr = document.createElement("tr"); - - td = document.createElement("td"); - - if (obj[i].time < 1563300000) { - td.innerHTML = "(" + obj[i].time + ")"; - } else { - var vuepoch = new Date(obj[i].time * 1000); - td.innerHTML = vuepoch.getUTCFullYear() + - "-" + twoDigits(vuepoch.getUTCMonth() + 1) + - "-" + twoDigits(vuepoch.getUTCDate()) + - " " + twoDigits(vuepoch.getUTCHours()) + - ":" + twoDigits(vuepoch.getUTCMinutes()) + - ":" + twoDigits(vuepoch.getUTCSeconds()); - } - tr.appendChild(td); - - td = document.createElement("td"); - td.innerHTML = obj[i].topic - tr.appendChild(td); - - td = document.createElement("td"); - td.innerHTML = obj[i].payload - tr.appendChild(td); - - mtable.appendChild(tr); - } } function getContent(contentname) { diff --git a/tools/wsemulator/wserver.js b/tools/wsemulator/wserver.js index 8f5353f82..6ffc71f07 100644 --- a/tools/wsemulator/wserver.js +++ b/tools/wsemulator/wserver.js @@ -97,8 +97,9 @@ var custom_configfile = { "listen_mode": false, "shower_timer": true, "shower_alert": false, - "publish_time": 0, - "tx_mode": 1 + "publish_time": 10, + "tx_mode": 1, + "bus_id": 11 } }; @@ -118,17 +119,7 @@ function sendStatus() { "systemload": 0, "mqttconnected": true, "mqttheartbeat": false, - "uptime": "0 days 0 hours 1 minute 45 seconds", - "mqttloghdr": "home/ems-esp/", - "mqttlog": [ - { "topic": "start", "payload": "start", "time": 1565956388 }, - { "topic": "shower_timer", "payload": "1", "time": 1565956388 }, - { "topic": "shower_alert", "payload": "0", "time": 1565956388 }, - { "topic": "boiler_data", "payload": "{\"wWComfort\":\"Hot\",\"wWSelTemp\":60,\"selFlowTemp\":5,\"selBurnPow\":0,\"curBurnPow\":0,\"pumpMod\":0,\"wWCurTmp\":48.4,\"wWCurFlow\":0,\"curFlowTemp\":49.3,\"retTemp\":49.3,\"sysPress\":1.8,\"boilTemp\":50.5,\"wWActivated\":\"on\",\"burnGas\":\"off\",\"heatPmp\":\"off\",\"fanWork\":\"off\",\"ignWork\":\"off\",\"wWCirc\":\"off\",\"wWHeat\":\"on\",\"burnStarts\":223397,\"burnWorkMin\":366019,\"heatWorkMin\":294036,\"ServiceCode\":\"0H\",\"ServiceCodeNumber\":203}", "time": 1565956463 }, - { "topic": "tapwater_active", "payload": "0", "time": 1565956408 }, - { "topic": "heating_active", "payload": "0", "time": 1565956408 }, - { "topic": "thermostat_data", "payload": "{\"thermostat_hc\":\"1\",\"thermostat_seltemp\":15,\"thermostat_currtemp\":23,\"thermostat_mode\":\"auto\"}", "time": 1565956444 } - ] + "uptime": "0 days 0 hours 1 minute 45 seconds" }; wss.broadcast(stats); @@ -170,8 +161,7 @@ function sendCustomStatus() { "b2": "off", "b3": 0, "b4": 53, - "b5": 54.4, - "b6": 53.3 + "b5": 54.4 }, "sm": {