diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..ee49bb82b --- /dev/null +++ b/.clang-format @@ -0,0 +1,39 @@ +Language: Cpp +BasedOnStyle: LLVM +UseTab: Never +IndentWidth: 4 +ColumnLimit: 140 +TabWidth: 4 +#BreakBeforeBraces: Custom +BraceWrapping: + AfterControlStatement: false + AfterFunction: false + AfterClass: true + AfterEnum: true + BeforeElse: false +ReflowComments: false +AlignAfterOpenBracket: Align # If true, horizontally aligns arguments after an open bracket. +AlignConsecutiveAssignments: true # This will align the assignment operators of consecutive lines +AlignConsecutiveDeclarations: true # This will align the declaration names of consecutive lines +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +#AlwaysBreakAfterReturnType: TopLevel +AlwaysBreakTemplateDeclarations: true # If true, always break after the template<...> of a template declaration +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: NonAssignment +BreakConstructorInitializersBeforeComma: true # Always break constructor initializers before commas and align the commas with the colon. +ExperimentalAutoDetectBinPacking: false +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 4 +PenaltyBreakBeforeFirstCallParameter: 200 +PenaltyExcessCharacter: 10 +PointerAlignment: Middle +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index ec2cedd63..70ec5cfc7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ platformio.ini lib/readme.txt .travis.yml -.clang-format diff --git a/CHANGELOG.md b/CHANGELOG.md index 452c35cfa..723c5fe5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.2.0] 2019-01-01 + +### Fixed + +- Incorrect indenting in `climate.yaml` (thanks @mrfixit1) +- Improved support for slower WiFi connections +- Fixed issue with OTA not always giving back a completion response to platformio +- Fixed issue with repeating reads after a raw mode send +- Fixed handling of long integers (thanks @SpaceTeddy) ### Added -- Setting the mode and setpoint temperature on a RC35 +- added 'dout' flashmode to platformio.ini so OTA works now when uploading to a Wemos D1 Pro's or any other board with larger flash's +- added un tested supporting RC35 type of thermostats +- Try and discover and set Boiler and Thermostat types automatically +- Fetch UBATotalUptimeMessage from Boiler to get total working minutes +- Added check to see if bus is connected. Shown in stats page +- If no Wifi connection can be made, start up as a WiFi Access Point (AP) +- Report out service codes and water-flow [pull-request](https://github.com/proddy/EMS-ESP-Boiler/pull/20/files). Thanks @Bonusbartus + +### Changed + +- Build option is called `DEBUG_SUPPORT` (was `USE_SERIAL`) +- Replaced old **ESPHelper** with my own **MyESP** library to handle Wifi, MQTT, MDNS and Telnet handlers. Supports asynchronous TCP and has smaller memory footprint. And moved to libs directory. +- Simplified LED error checking. If enabled (by default), solid means connected and flashing means error. Uses either an external pull-up or the onboard ESP8266 LED. +- Improved Telnet debugging which uses TelnetSpy to keep a buffer of previous output +- Optimized memory usage & heap conflicts, removing nasty things like strcpy, sprintf where possible +- Improved checking for tap water on/off (thanks @Bonusbartus) + +### Removed + +- Time and TimeLib's. Not used in code. +- Removed build option `MQTT_MAX_PACKAGE_SIZE` as not using the PubSubClient library any more +- Removed all of Espurna's pre-built firmwares and instructions to build. Keeping it simple. ## [1.1.1] 2018-12-23 @@ -21,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixed handling of negative flaoting point values (like outdoor temp) +- Fixed handling of negative floating point values (like outdoor temp) - Fixed handling of auto & manual mode on an RC30 - [Fixed condition where all telegram types were processed, instead of only broadcasts or our own reads](https://github.com/proddy/EMS-ESP-Boiler/issues/15) diff --git a/README.md b/README.md index 2b79fa89b..f150825db 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ EMS-ESP-Boiler is a project to build a controller circuit running with an ESP826 There are 3 parts to this project, first the design of the circuit, second the code for the ESP8266 microcontroller firmware and lastly an example configuration for Home Assistant to monitor the data and issue direct commands via MQTT. -[![version](https://img.shields.io/badge/version-1.1.0-brightgreen.svg)](CHANGELOG.md) +[![version](https://img.shields.io/badge/version-1.1.2-brightgreen.svg)](CHANGELOG.md) - [EMS-ESP-Boiler](#ems-esp-boiler) - [Introduction](#introduction) @@ -33,6 +33,7 @@ There are 3 parts to this project, first the design of the circuit, second the c - [Building The Firmware](#building-the-firmware) - [Using PlatformIO Standalone](#using-platformio-standalone) - [Building Using Arduino IDE](#building-using-arduino-ide) + - [Troubleshooting](#troubleshooting) - [Known Issues](#known-issues) - [Wish List](#wish-list) - [Your Comments and Feedback](#your-comments-and-feedback) @@ -208,9 +209,9 @@ The code is built on the Arduino framework and is dependent on these external li `ems.cpp` is the logic to read the EMS packets (telegrams), validates them and process them based on the type. -`boiler.ino` is the Arduino code for the ESP8266 that kicks it all off. This is where we have specific logic such as the code to monitor and alert on the Shower timer and light up the LEDs. LED support is enabled by setting the -DUSE_LED build flag. +`boiler.ino` is the Arduino code for the ESP8266 that kicks it all off. This is where we have specific logic such as the code to monitor and alert on the Shower timer and light up the LEDs. LED support is enabled by default and can be switched off at compile time using the -DNO_LED build flag. -`ESPHelper.cpp` is my customized version of [ESPHelper](https://github.com/ItKindaWorks/ESPHelper) with added Telnet support and some other minor tweaking. +`MyESP.cpp` is my custom library to handle WiFi, MQTT, MDNS and Telnet. Uses a modified version of TelnetSpy (https://github.com/yasheena/telnetspy) ### Supported EMS Types @@ -227,11 +228,20 @@ The code is built on the Arduino framework and is dependent on these external li | Thermostat | 0x02 | Version | reads Version major/minor | | Thermostat | 0x91, 0x41, 0x0A | Status Message | read monitor values | -In `boiler.ino` you can make calls to automatically send these read commands. See the function *regularUpdates()* +In `boiler.ino` you can make calls to automatically request these types in the function *regularUpdates()*. ### Supported Thermostats -Modify `EMS_ID_THERMOSTAT` in `myconfig.h` to the thermostat type you want to support. +I am still working on adding more support to known thermostats. + +Currently known types and collected versions: + +Moduline 300 = Type 77 Version 03.03 +Moduline 400 = Type 78 Version 03.03 +Buderus RC35 = Type 86 Version 01.15 +Nefit Easy = Type 202 Version 02.19 +Nefit Trendline HRC30 = Type 123 Version 06.01 +BC10 = Type 123 Version 04.05 #### RC20 (Moduline 300) @@ -245,8 +255,6 @@ Type's 3F, 49, 53, 5D are identical. So are 4B, 55, 5F and mostly zero's. Types #### RC35 -***not implemented yet***! - An RC35 thermostat can support up to 4 heating circuits each controlled with their own Monitor and Working Mode IDs. Fetching the thermostats setpoint temp us by requesting 0x3E and looking at the 3rd byte in the data telegram (data[2]) and dividing by 2. @@ -259,8 +267,7 @@ There is limited support for an Nefit Easy TC100/TC200 type thermostat. The curr ### Customizing The Code - To configure for your thermostat and specific boiler settings, modify `my_config.h`. Here you can - - set the thermostat type. The default ID is 0x17 for an RC30 Moduline 300. - - set flags for enabled/disabling functionality such as `BOILER_THERMOSTAT_ENABLED`, `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`. + - set flags for enabled/disabling functionality such as `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`. - Set WIFI and MQTT settings, instead of doing this in `platformio.ini` - To add new handlers for EMS data types, first create a callback function and add to the `EMS_Types` array at the top of the file `ems.cpp` and modify `ems.h` @@ -327,7 +334,7 @@ PlatformIO is my preferred way. The code uses a modified version [ESPHelper](htt % cd EMS-ESP-Boiler % cp platformio.ini-example platformio.ini ``` -- edit `platformio.ini` to set `env_default` and the flags `WIFI_SSID WIFI_PASSWORD, MQTT_IP, MQTT_USER, MQTT_PASS`. If you're not using MQTT leave MQTT_IP empty (`MQTT_IP=""`) +- edit `platformio.ini` to set `env_default` and the flags like `WIFI_SSID WIFI_PASSWORD, MQTT_IP, MQTT_USER, MQTT_PASS`. If you're not using MQTT leave MQTT_IP empty (`MQTT_IP=""`) ```c % platformio run -t upload ``` @@ -339,7 +346,7 @@ Porting to the Arduino IDE can be a little tricky but it is possible. - Add the ESP8266 boards (from Preferences add Additional Board URL `http://arduino.esp8266.com/stable/package_esp8266com_index.json`) - Go to Boards Manager and install ESP8266 2.4.x platform - Select your ESP8266 from Tools->Boards and the correct port with Tools->Port -- From the Library Manager install the needed libraries from platformio.ini such as ArduinoJson 5.13.x, PubSubClient 2.6.x, CRC32 and Time +- From the Library Manager install the needed libraries from platformio.ini - The Arduino IDE doesn't have a common way to set build flags (ugh!) so you'll need to un-comment these lines in `boiler.ino`: ```c @@ -353,6 +360,12 @@ Porting to the Arduino IDE can be a little tricky but it is possible. - Put all the files in a single sketch folder (`ESPHelper.*, boiler.ino, ems.*, emsuart.*`) - cross your fingers and hit CTRL-R to compile... +## Troubleshooting + +If the WiFi, MQTT, MDNS or something else fails to connect, re-build the firmware using the `-DDEBUG_SUPPORT` option, connect the ESP8266 to a USB in your computer and monitor the Serial output. A lot of detailed logging will be printed to help you pinpoint the cause of the error. + +The onboard LED will flash if there is no connection with the EMS bus. You can disable LED support by adding -DNO_LED to the build options. + ## Known Issues Some annoying issues that need fixing: @@ -364,7 +377,6 @@ Some annoying issues that need fixing: - Measure amount of gas in m3 per day for the hot water vs the central heating, and convert this into cost in Home Assistant - Support changing temps on an Nefit Easy. To do this you must send XMPP messages directly to the thermostat. See this project: https://github.com/robertklep/nefit-easy-core - Store custom params like wifi credentials, mqtt, thermostat type on ESP8266 using SPIFFS -- Automatic detection of thermostat type - Add support for a temperature sensor on the circuit (DS18B20) ## Your Comments and Feedback diff --git a/doc/espurna/example.PNG b/doc/espurna/example.PNG deleted file mode 100644 index d83b3eb9e..000000000 Binary files a/doc/espurna/example.PNG and /dev/null differ diff --git a/doc/home_assistant/climate.yaml b/doc/home_assistant/climate.yaml index 392021b80..704be4762 100644 --- a/doc/home_assistant/climate.yaml +++ b/doc/home_assistant/climate.yaml @@ -1,20 +1,20 @@ - platform: mqtt - name: Thermostat - modes: - - low - - manual - - auto + name: Thermostat + modes: + - low + - manual + - auto - mode_state_topic: "home/boiler/thermostat_data" - current_temperature_topic: "home/boiler/thermostat_data" - temperature_state_topic: "home/boiler/thermostat_data" + mode_state_topic: "home/boiler/thermostat_data" + current_temperature_topic: "home/boiler/thermostat_data" + temperature_state_topic: "home/boiler/thermostat_data" - temperature_command_topic: "home/boiler/thermostat_cmd_temp" - mode_command_topic: "home/boiler/thermostat_cmd_mode" + temperature_command_topic: "home/boiler/thermostat_cmd_temp" + mode_command_topic: "home/boiler/thermostat_cmd_mode" - mode_state_template: "{{ value_json.thermostat_mode }}" - current_temperature_template: "{{ value_json.thermostat_currtemp }}" - temperature_state_template: "{{ value_json.thermostat_seltemp }}" + mode_state_template: "{{ value_json.thermostat_mode }}" + current_temperature_template: "{{ value_json.thermostat_currtemp }}" + temperature_state_template: "{{ value_json.thermostat_seltemp }}" - temp_step: 0.5 + temp_step: 0.5 diff --git a/doc/home_assistant/sensors.yaml b/doc/home_assistant/sensors.yaml index ecbe341a1..330167956 100644 --- a/doc/home_assistant/sensors.yaml +++ b/doc/home_assistant/sensors.yaml @@ -42,6 +42,12 @@ unit_of_measurement: '°C' value_template: '{{ value_json.wWSelTemp }}' +- platform: mqtt + state_topic: 'home/boiler/boiler_data' + name: 'Warm Water tapwater flow rate' + unit_of_measurement: 'l/min' + value_template: '{{ value_json.wWCurFlow }}' + - platform: mqtt state_topic: 'home/boiler/boiler_data' name: 'Warm Water current temperature' diff --git a/doc/home_assistant/ui-lovelace.yaml b/doc/home_assistant/ui-lovelace.yaml index e144c7344..4fafbdcbd 100644 --- a/doc/home_assistant/ui-lovelace.yaml +++ b/doc/home_assistant/ui-lovelace.yaml @@ -14,6 +14,7 @@ views: - sensor.warm_water_current_temperature - sensor.warm_water_activated - sensor.warm_water_3way_valve + - sensor.warm_water_tapwater_flow_rate - type: divider - sensor.boiler_temperature - sensor.return_temperature diff --git a/extra_script.py b/extra_script.py index 21eb27d66..4d93ae919 100644 --- a/extra_script.py +++ b/extra_script.py @@ -1,12 +1,28 @@ +#!/usr/bin/env python +from subprocess import call +import os Import("env") + +def code_check(source, target, env): + print("\n** Starting cppcheck...") + call(["cppcheck", os.getcwd()+"/.", "--force", "--enable=all"]) + print("\n** Finished cppcheck...\n") + print("\n** Starting cpplint...") + call(["cpplint", "--extensions=ino,cpp,h", "--filter=-legal/copyright,-build/include,-whitespace", + "--linelength=120", "--recursive", "src"]) + print("\n** Finished cpplint...") + #my_flags = env.ParseFlags(env['BUILD_FLAGS']) #defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")} # print defines - -#env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION")) - # print env.Dump() -env.Replace(PROGNAME="firmware_%s" % env['BOARD']) +# built in targets: (buildprog, size, upload, program, buildfs, uploadfs, uploadfsota) +env.AddPreAction("buildprog", code_check) +# env.AddPostAction(.....) + +# see http://docs.platformio.org/en/latest/projectconf/advanced_scripting.html#before-pre-and-after-post-actions +# env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION")) +# env.Replace(PROGNAME="firmware_%s" % env['BOARD']) diff --git a/lib/TelnetSpy/TelnetSpy.cpp b/lib/TelnetSpy/TelnetSpy.cpp new file mode 100644 index 000000000..3cf582da6 --- /dev/null +++ b/lib/TelnetSpy/TelnetSpy.cpp @@ -0,0 +1,626 @@ +/* + * TELNET SERVER FOR ESP8266 / ESP32 + * Cloning the serial port via Telnet. + * + * Written by Wolfgang Mattis (arduino@yasheena.de). + * Version 1.1 / September 7, 2018. + * MIT license, all text above must be included in any redistribution. + */ + +#ifdef ESP8266 +extern "C" { +#include "user_interface.h" +} +#endif + +#include "TelnetSpy.h" + +#ifndef min +#define min(a, b) ((a) < (b) ? (a) : (b)) +#endif +#ifndef max +#define max(a, b) ((a) > (b) ? (a) : (b)) +#endif + +static TelnetSpy * actualObject = NULL; + +static void TelnetSpy_putc(char c) { + if (actualObject) { + actualObject->write(c); + } +} + +static void TelnetSpy_ignore_putc(char c) { + ; +} + +TelnetSpy::TelnetSpy() { + port = TELNETSPY_PORT; + telnetServer = NULL; + started = false; + listening = false; + firstMainLoop = true; + usedSer = &Serial; + storeOffline = true; + connected = false; + callbackConnect = NULL; + callbackDisconnect = NULL; + welcomeMsg = strdup(TELNETSPY_WELCOME_MSG); + rejectMsg = strdup(TELNETSPY_REJECT_MSG); + minBlockSize = TELNETSPY_MIN_BLOCK_SIZE; + collectingTime = TELNETSPY_COLLECTING_TIME; + maxBlockSize = TELNETSPY_MAX_BLOCK_SIZE; + pingTime = TELNETSPY_PING_TIME; + pingRef = 0xFFFFFFFF; + waitRef = 0xFFFFFFFF; + telnetBuf = NULL; + bufLen = 0; + uint16_t size = TELNETSPY_BUFFER_LEN; + while (!setBufferSize(size)) { + size = size >> 1; + if (size < minBlockSize) { + setBufferSize(minBlockSize); + break; + } + } + debugOutput = TELNETSPY_CAPTURE_OS_PRINT; + if (debugOutput) { + setDebugOutput(true); + } +} + +TelnetSpy::~TelnetSpy() { + end(); +} + +// added by proddy +void TelnetSpy::disconnectClient() { + if (client.connected()) { + client.flush(); + client.stop(); + } + if (connected && (callbackDisconnect != NULL)) { + callbackDisconnect(); + } + connected = false; +} + +void TelnetSpy::setPort(uint16_t portToUse) { + port = portToUse; + if (listening) { + if (client.connected()) { + client.flush(); + client.stop(); + } + if (connected && (callbackDisconnect != NULL)) { + callbackDisconnect(); + } + connected = false; + telnetServer->close(); + delete telnetServer; + telnetServer = new WiFiServer(port); + if (started) { + telnetServer->begin(); + telnetServer->setNoDelay(bufLen > 0); + } + } +} + +void TelnetSpy::setWelcomeMsg(char * msg) { + if (welcomeMsg) { + free(welcomeMsg); + } + welcomeMsg = strdup(msg); +} + +void TelnetSpy::setRejectMsg(char * msg) { + if (rejectMsg) { + free(rejectMsg); + } + rejectMsg = strdup(msg); +} + +void TelnetSpy::setMinBlockSize(uint16_t minSize) { + minBlockSize = min(max((uint16_t)1, minSize), maxBlockSize); +} + +void TelnetSpy::setCollectingTime(uint16_t colTime) { + collectingTime = colTime; +} + +void TelnetSpy::setMaxBlockSize(uint16_t maxSize) { + maxBlockSize = max(maxSize, minBlockSize); +} + +bool TelnetSpy::setBufferSize(uint16_t newSize) { + if (telnetBuf && (bufLen == newSize)) { + return true; + } + if (newSize == 0) { + bufLen = 0; + if (telnetBuf) { + free(telnetBuf); + telnetBuf = NULL; + } + if (telnetServer) { + telnetServer->setNoDelay(false); + } + return true; + } + newSize = max(newSize, minBlockSize); + uint16_t oldBufLen = bufLen; + bufLen = newSize; + uint16_t tmp; + if (!telnetBuf || (bufUsed == 0)) { + bufRdIdx = 0; + bufWrIdx = 0; + bufUsed = 0; + } else { + if (bufLen < oldBufLen) { + if (bufRdIdx < bufWrIdx) { + if (bufWrIdx > bufLen) { + tmp = min(bufLen, (uint16_t)(bufWrIdx - max(bufLen, bufRdIdx))); + memcpy(telnetBuf, &telnetBuf[bufWrIdx - tmp], tmp); + bufWrIdx = tmp; + if (bufWrIdx > bufRdIdx) { + bufRdIdx = bufWrIdx; + } else { + if (bufRdIdx > bufLen) { + bufRdIdx = 0; + } + } + if (bufRdIdx == bufWrIdx) { + bufUsed = bufLen; + } else { + bufUsed = bufWrIdx - bufRdIdx; + } + } + } else { + if (bufWrIdx > bufLen) { + memcpy(telnetBuf, &telnetBuf[bufWrIdx - bufLen], bufLen); + bufRdIdx = 0; + bufWrIdx = 0; + bufUsed = bufLen; + } else { + tmp = min(bufLen - bufWrIdx, oldBufLen - bufRdIdx); + memcpy(&telnetBuf[bufLen - tmp], &telnetBuf[oldBufLen - tmp], tmp); + bufRdIdx = bufLen - tmp; + bufUsed = bufWrIdx + tmp; + } + } + } + } + char * temp = (char *)realloc(telnetBuf, bufLen); + if (!temp) { + return false; + } + telnetBuf = temp; + if (telnetBuf && (bufLen > oldBufLen) && (bufRdIdx > bufWrIdx)) { + tmp = bufLen - (oldBufLen - bufRdIdx); + memcpy(&telnetBuf[tmp], &telnetBuf[bufRdIdx], oldBufLen - bufRdIdx); + bufRdIdx = tmp; + } + if (telnetServer) { + telnetServer->setNoDelay(true); + } + return true; +} + +uint16_t TelnetSpy::getBufferSize() { + if (!telnetBuf) { + return 0; + } + return bufLen; +} + +void TelnetSpy::setStoreOffline(bool store) { + storeOffline = store; +} + +bool TelnetSpy::getStoreOffline() { + return storeOffline; +} + +void TelnetSpy::setPingTime(uint16_t pngTime) { + pingTime = pngTime; + if (pingTime == 0) { + pingRef = 0xFFFFFFFF; + } else { + pingRef = (millis() & 0x7FFFFFF) + pingTime; + } +} + +void TelnetSpy::setSerial(HardwareSerial * usedSerial) { + usedSer = usedSerial; +} + +size_t TelnetSpy::write(uint8_t data) { + if (telnetBuf) { + if (storeOffline || client.connected()) { + if (bufUsed == bufLen) { + if (client.connected()) { + sendBlock(); + } + if (bufUsed == bufLen) { + char c; + while (bufUsed > 0) { + c = pullTelnetBuf(); + if (c == '\n') { + addTelnetBuf('\r'); + break; + } + } + if (peekTelnetBuf() == '\r') { + pullTelnetBuf(); + } + } + } + addTelnetBuf(data); + /* + if (data == '\n') { + addTelnetBuf('\r'); // added by proddy, fix for Windows + } + */ + } + } else { + if (client.connected()) { + client.write(data); + } + } + if (usedSer) { + return usedSer->write(data); + } + return 1; +} + +int TelnetSpy::available(void) { + if (usedSer) { + int avail = usedSer->available(); + if (avail > 0) { + return avail; + } + } + if (client.connected()) { + return telnetAvailable(); + } + return 0; +} + +int TelnetSpy::read(void) { + int val; + if (usedSer) { + val = usedSer->read(); + if (val != -1) { + return val; + } + } + if (client.connected()) { + if (telnetAvailable()) { + val = client.read(); + } + } + return val; +} + +int TelnetSpy::peek(void) { + int val; + if (usedSer) { + val = usedSer->peek(); + if (val != -1) { + return val; + } + } + if (client.connected()) { + if (telnetAvailable()) { + val = client.peek(); + } + } + return val; +} + +void TelnetSpy::flush(void) { + if (usedSer) { + usedSer->flush(); + } +} + +#ifdef ESP8266 + +void TelnetSpy::begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin) { + if (usedSer) { + usedSer->begin(baud, config, mode, tx_pin); + } + started = true; +} + +#else // ESP32 + +void TelnetSpy::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert) { + if (usedSer) { + usedSer->begin(baud, config, rxPin, txPin, invert); + } + started = true; +} + +#endif + +void TelnetSpy::end() { + if (debugOutput) { + setDebugOutput(false); + } + if (usedSer) { + usedSer->end(); + } + if (client.connected()) { + client.flush(); + client.stop(); + } + if (connected && (callbackDisconnect != NULL)) { + callbackDisconnect(); + } + connected = false; + telnetServer->close(); + delete telnetServer; + telnetServer = NULL; + listening = false; + started = false; +} + +#ifdef ESP8266 + +void TelnetSpy::swap(uint8_t tx_pin) { + if (usedSer) { + usedSer->swap(tx_pin); + } +} + +void TelnetSpy::set_tx(uint8_t tx_pin) { + if (usedSer) { + usedSer->set_tx(tx_pin); + } +} + +void TelnetSpy::pins(uint8_t tx, uint8_t rx) { + if (usedSer) { + usedSer->pins(tx, rx); + } +} + +bool TelnetSpy::isTxEnabled(void) { + if (usedSer) { + return usedSer->isTxEnabled(); + } + return true; +} + +bool TelnetSpy::isRxEnabled(void) { + if (usedSer) { + return usedSer->isRxEnabled(); + } + return true; +} + +#endif + +int TelnetSpy::availableForWrite(void) { + if (usedSer) { + return min(usedSer->availableForWrite(), bufLen - bufUsed); + } + return bufLen - bufUsed; +} + +TelnetSpy::operator bool() const { + if (usedSer) { + return (bool)*usedSer; + } + return true; +} + +void TelnetSpy::setDebugOutput(bool en) { + debugOutput = en; + if (debugOutput) { + actualObject = this; +#ifdef ESP8266 + os_install_putc1((void *)TelnetSpy_putc); // Set system printing (os_printf) to TelnetSpy + system_set_os_print(true); +#else // ESP32 \ + // ToDo: How can be done this for ESP32 ? +#endif + } else { + if (actualObject == this) { +#ifdef ESP8266 + system_set_os_print(false); + os_install_putc1((void *)TelnetSpy_ignore_putc); // Ignore system printing +#else // ESP32 \ + // ToDo: How can be done this for ESP32 ? +#endif + actualObject = NULL; + } + } +} + +uint32_t TelnetSpy::baudRate(void) { + if (usedSer) { + return usedSer->baudRate(); + } + return 115200; +} + +void TelnetSpy::sendBlock() { + uint16_t len = bufUsed; + if (len > maxBlockSize) { + len = maxBlockSize; + } + len = min(len, (uint16_t)(bufLen - bufRdIdx)); + client.write(&telnetBuf[bufRdIdx], len); + bufRdIdx += len; + if (bufRdIdx >= bufLen) { + bufRdIdx = 0; + } + bufUsed -= len; + if (bufUsed == 0) { + bufRdIdx = 0; + bufWrIdx = 0; + } + waitRef = 0xFFFFFFFF; + if (pingRef != 0xFFFFFFFF) { + pingRef = (millis() & 0x7FFFFFF) + pingTime; + if (pingRef > 0x7FFFFFFF) { + pingRef -= 0x80000000; + } + } +} + +void TelnetSpy::addTelnetBuf(char c) { + telnetBuf[bufWrIdx] = c; + if (bufUsed == bufLen) { + bufRdIdx++; + if (bufRdIdx >= bufLen) { + bufRdIdx = 0; + } + } else { + bufUsed++; + } + bufWrIdx++; + if (bufWrIdx >= bufLen) { + bufWrIdx = 0; + } +} + +char TelnetSpy::pullTelnetBuf() { + if (bufUsed == 0) { + return 0; + } + char c = telnetBuf[bufRdIdx++]; + if (bufRdIdx >= bufLen) { + bufRdIdx = 0; + } + bufUsed--; + return c; +} + +char TelnetSpy::peekTelnetBuf() { + if (bufUsed == 0) { + return 0; + } + return telnetBuf[bufRdIdx]; +} + +int TelnetSpy::telnetAvailable() { + int n = client.available(); + while (n > 0) { + if (0xff == client.peek()) { // If esc char for telnet NVT protocol data remove that telegram: + client.read(); // Remove esc char + n--; + if (0xff == client.peek()) { // If esc sequence for 0xFF data byte... + return n; // ...return info about available data (just this 0xFF data byte) + } + client.read(); // Skip the rest of the telegram of the telnet NVT protocol data + client.read(); + n--; + n--; + } else { // If next char is a normal data byte... + return n; // ...return info about available data + } + } + return 0; +} + +bool TelnetSpy::isClientConnected() { + return connected; +} + +void TelnetSpy::setCallbackOnConnect(telnetSpyCallback callback) { + callbackConnect = callback; +} + +void TelnetSpy::setCallbackOnDisconnect(telnetSpyCallback callback) { + callbackDisconnect = callback; +} + +void TelnetSpy::handle() { + if (firstMainLoop) { + firstMainLoop = false; + // Between setup() and loop() the configuration for os_print may be changed so it must be renewed + if (debugOutput && (actualObject == this)) { + setDebugOutput(true); + } + } + if (!started) { + return; + } + if (!listening) { + if (WiFi.status() != WL_CONNECTED) { + return; + } + telnetServer = new WiFiServer(port); + telnetServer->begin(); + telnetServer->setNoDelay(bufLen > 0); + listening = true; + if (usedSer) { + usedSer->println("[TELNET] Telnet server started"); // added by Proddy + } + } + if (telnetServer->hasClient()) { + if (client.connected()) { + WiFiClient rejectClient = telnetServer->available(); + if (strlen(rejectMsg) > 0) { + rejectClient.write((const uint8_t *)rejectMsg, strlen(rejectMsg)); + } + rejectClient.flush(); + rejectClient.stop(); + } else { + client = telnetServer->available(); + if (strlen(welcomeMsg) > 0) { + client.write((const uint8_t *)welcomeMsg, strlen(welcomeMsg)); + } + } + } + if (client.connected()) { + if (!connected) { + connected = true; + if (pingTime != 0) { + pingRef = (millis() & 0x7FFFFFF) + pingTime; + } + if (callbackConnect != NULL) { + callbackConnect(); + } + } + } else { + if (connected) { + connected = false; + client.flush(); + client.stop(); + pingRef = 0xFFFFFFFF; + waitRef = 0xFFFFFFFF; + if (callbackDisconnect != NULL) { + callbackDisconnect(); + } + } + } + + if (client.connected() && (bufUsed > 0)) { + if (bufUsed >= minBlockSize) { + sendBlock(); + } else { + unsigned long m = millis() & 0x7FFFFFF; + if (waitRef == 0xFFFFFFFF) { + waitRef = m + collectingTime; + if (waitRef > 0x7FFFFFFF) { + waitRef -= 0x80000000; + } + } else { + if (!((waitRef < 0x20000000) && (m > 0x60000000)) && (m >= waitRef)) { + sendBlock(); + } + } + } + } + if (client.connected() && (pingRef != 0xFFFFFFFF)) { + unsigned long m = millis() & 0x7FFFFFF; + if (!((pingRef < 0x20000000) && (m > 0x60000000)) && (m >= pingRef)) { + addTelnetBuf(0); + sendBlock(); + } + } +} diff --git a/lib/TelnetSpy/TelnetSpy.h b/lib/TelnetSpy/TelnetSpy.h new file mode 100644 index 000000000..51fa4cb83 --- /dev/null +++ b/lib/TelnetSpy/TelnetSpy.h @@ -0,0 +1,278 @@ +/* + * TELNET SERVER FOR ESP8266 / ESP32 + * Cloning the serial port via Telnet. + * + * Written by Wolfgang Mattis (arduino@yasheena.de). + * Version 1.1 / September 7, 2018. + * MIT license, all text above must be included in any redistribution. + */ + +/* + * DESCRIPTION + * + * This module allows you "Debugging over the air". So if you already use + * ArduinoOTA this is a helpful extension for wireless development. Use + * "TelnetSpy" instead of "Serial" to send data to the serial port and a copy + * to a telnet connection. There is a circular buffer which allows to store the + * data while the telnet connection is not established. So its possible to + * collect data even when the Wifi and Telnet connections are still not + * established. Its also possible to create a telnet session only if it is + * neccessary: then you will get the already collected data as far as it is + * still stored in the circular buffer. Data send from telnet terminal to + * ESP8266 / ESP32 will be handled as data received by serial port. It is also + * possible to use more than one instance of TelnetSpy, for example to send + * control information on the first instance and data dumps on the second + * instance. + * + * USAGE + * + * Add the following line to your sketch: + * #include + * TelnetSpy LOG; + * + * Add the following line to your initialisation block ( void setup() ): + * LOG.begin(); + * + * Add the following line at the beginning of your main loop ( void loop() ): + * LOG.handle(); + * + * Use the following functions of the TelnetSpy object to modify behavior + * + * Change the port number of this telnet server. If a client is already + * connected it will be disconnected. + * Default: 23 + * void setPort(uint16_t portToUse); + * + * Change the message which will be send to the telnet client after a session + * is established. + * Default: "Connection established via TelnetSpy.\n" + * void setWelcomeMsg(char* msg); + * + * Change the message which will be send to the telnet client if another + * session is already established. + * Default: "TelnetSpy: Only one connection possible.\n" + * void setRejectMsg(char* msg); + * + * Change the amount of characters to collect before sending a telnet block. + * Default: 64 + * void setMinBlockSize(uint16_t minSize); + * + * Change the time (in ms) to wait before sending a telnet block if its size is + * less than (defined by setMinBlockSize). + * Default: 100 + * void setCollectingTime(uint16_t colTime); + * + * Change the maximum size of the telnet packets to send. + * Default: 512 + * void setMaxBlockSize(uint16_t maxSize); + * + * Change the size of the ring buffer. Set it to 0 to disable buffering. + * Changing size tries to preserve the already collected data. If the new + * buffer size is too small the youngest data will be preserved only. Returns + * false if the requested buffer size cannot be set. + * Default: 3000 + * bool setBufferSize(uint16_t newSize); + * + * This function returns the actual size of the ring buffer. + * uint16_t getBufferSize(); + * + * Enable / disable storing new data in the ring buffer if no telnet connection + * is established. This function allows you to store important data only. You + * can do this by disabling "storeOffline" for sending less important data. + * Default: true + * void setStoreOffline(bool store); + * + * Get actual state of storing data when offline. + * bool getStoreOffline(); + * + * If no data is sent via TelnetSpy the detection of a disconnected client has + * a long timeout. Use setPingTime to define the time (in ms) without traffic + * after which a ping (chr(0)) is sent to the telnet client to detect a + * disconnect earlier. Use 0 as parameter to disable pings. + * Default: 1500 + * void setPingTime(uint16_t pngTime); + * + * Set the serial port you want to use with this object (especially for ESP32) + * or NULL if no serial port should be used (telnet only). + * Default: Serial + * void setSerial(HardwareSerial* usedSerial); + * + * This function returns true, if a telnet client is connected. + * bool isClientConnected(); + * + * This function installs a callback function which will be called on every + * telnet connect of this object (except rejected connect tries). Use NULL to + * remove the callback. + * Default: NULL + * void setCallbackOnConnect(void (*callback)()); + * + * This function installs a callback function which will be called on every + * telnet disconnect of this object (except rejected connect tries). Use NULL + * to remove the callback. + * Default: NULL + * void setCallbackOnDisconnect(void (*callback)()); + * + * HINT + * + * Add the following lines to your sketch: + * #define SERIAL TelnetSpy + * //#define SERIAL Serial + * + * Replace "Serial" with "SERIAL" in your sketch. Now you can switch between + * serial only and serial with telnet by changing the comments of the defines + * only. + * + * IMPORTANT + * + * To connect to the telnet server you have to: + * - establish the Wifi connection + * - execute "TelnetSpy.begin(WhatEverYouWant);" + * + * The order is not important. + * + * All you do with "Serial" you can also do with "TelnetSpy", but remember: + * Transfering data also via telnet will need more performance than the serial + * port only. So time critical things may be influenced. + * + * It is not possible to establish more than one telnet connection at the same + * time. But its possible to use more than one instance of TelnetSpy. + * + * If you have problems with low memory you may reduce the value of the define + * TELNETSPY_BUFFER_LEN for a smaller ring buffer on initialisation. + * + * Usage of void setDebugOutput(bool) to enable / disable of capturing of + * os_print calls when you have more than one TelnetSpy instance: That + * TelnetSpy object will handle this functionality where you used + * setDebugOutput at last. On default TelnetSpy has the capturing of OS_print + * calls enabled. So if you have more instances the last created instance will + * handle the capturing. + */ + +#ifndef TelnetSpy_h +#define TelnetSpy_h + +#define TELNETSPY_BUFFER_LEN 3000 +#define TELNETSPY_MIN_BLOCK_SIZE 64 +#define TELNETSPY_COLLECTING_TIME 100 +#define TELNETSPY_MAX_BLOCK_SIZE 512 +#define TELNETSPY_PING_TIME 1500 +#define TELNETSPY_PORT 23 +#define TELNETSPY_CAPTURE_OS_PRINT true +#define TELNETSPY_WELCOME_MSG "Connection established via TelnetSpy2.\n" +#define TELNETSPY_REJECT_MSG "TelnetSpy: Only one connection possible.\n" + +#ifdef ESP8266 +#include +#else // ESP32 +#include +#endif +#include + +class TelnetSpy : public Stream { + public: + TelnetSpy(); + ~TelnetSpy(); + void handle(void); + void setPort(uint16_t portToUse); + void setWelcomeMsg(char * msg); + void setRejectMsg(char * msg); + void setMinBlockSize(uint16_t minSize); + void setCollectingTime(uint16_t colTime); + void setMaxBlockSize(uint16_t maxSize); + bool setBufferSize(uint16_t newSize); + uint16_t getBufferSize(); + void setStoreOffline(bool store); + bool getStoreOffline(); + void setPingTime(uint16_t pngTime); + void setSerial(HardwareSerial * usedSerial); + bool isClientConnected(); + + void disconnectClient(); // added by Proddy + typedef std::function telnetSpyCallback; // added by Proddy + void setCallbackOnConnect(telnetSpyCallback callback); // changed by proddy + void setCallbackOnDisconnect(telnetSpyCallback callback); // changed by proddy + + // Functions offered by HardwareSerial class: +#ifdef ESP8266 + void begin(unsigned long baud) { + begin(baud, SERIAL_8N1, SERIAL_FULL, 1); + } + void begin(unsigned long baud, SerialConfig config) { + begin(baud, config, SERIAL_FULL, 1); + } + void begin(unsigned long baud, SerialConfig config, SerialMode mode) { + begin(baud, config, mode, 1); + } + void begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin); +#else // ESP32 + void begin(unsigned long baud, uint32_t config = SERIAL_8N1, int8_t rxPin = -1, int8_t txPin = -1, bool invert = false); +#endif + void end(); +#ifdef ESP8266 + void swap() { + swap(1); + } + void swap(uint8_t tx_pin); + void set_tx(uint8_t tx_pin); + void pins(uint8_t tx, uint8_t rx); + bool isTxEnabled(void); + bool isRxEnabled(void); +#endif + int available(void) override; + int peek(void) override; + int read(void) override; + int availableForWrite(void); + void flush(void) override; + size_t write(uint8_t) override; + inline size_t write(unsigned long n) { + return write((uint8_t)n); + } + inline size_t write(long n) { + return write((uint8_t)n); + } + inline size_t write(unsigned int n) { + return write((uint8_t)n); + } + inline size_t write(int n) { + return write((uint8_t)n); + } + using Print::write; + operator bool() const; + void setDebugOutput(bool); + uint32_t baudRate(void); + + protected: + void sendBlock(void); + void addTelnetBuf(char c); + char pullTelnetBuf(); + char peekTelnetBuf(); + int telnetAvailable(); + WiFiServer * telnetServer; + WiFiClient client; + uint16_t port; + HardwareSerial * usedSer; + bool storeOffline; + bool started; + bool listening; + bool firstMainLoop; + unsigned long waitRef; + unsigned long pingRef; + uint16_t pingTime; + char * welcomeMsg; + char * rejectMsg; + uint16_t minBlockSize; + uint16_t collectingTime; + uint16_t maxBlockSize; + bool debugOutput; + char * telnetBuf; + uint16_t bufLen; + uint16_t bufUsed; + uint16_t bufRdIdx; + uint16_t bufWrIdx; + bool connected; + + telnetSpyCallback callbackConnect; // added by proddy + telnetSpyCallback callbackDisconnect; // added by proddy +}; + +#endif diff --git a/lib/myESP/MyESP.cpp b/lib/myESP/MyESP.cpp new file mode 100644 index 000000000..96b3a3a9b --- /dev/null +++ b/lib/myESP/MyESP.cpp @@ -0,0 +1,617 @@ +/* + * MyESP - my ESP helper class to handle Wifi, MDNS, MQTT and Telnet + * + * Paul Derbyshire - December 2018 + * + * Some ideas from https://github.com/JoaoLopesF/ESP8266-RemoteDebug-Telnet + * Ideas from Espurna https://github.com/xoseperez/espurna + */ + +#include "MyESP.h" + +// constructor +MyESP::MyESP() { + _app_hostname = strdup("MyESP"); + _app_name = strdup("MyESP"); + _app_version = strdup("1.0.0"); + _boottime = strdup("unknown"); + _extern_WIFICallback = NULL; + _extern_WIFICallbackSet = false; + _consoleCallbackProjectCmds = NULL; + _helpProjectCmds = NULL; + _helpProjectCmds_count = 0; + _mqtt_host = NULL; + _mqtt_password = NULL; + _mqtt_username = NULL; + _wifi_password = NULL; + _wifi_ssid = NULL; + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + _verboseMessages = true; + _command = (char *)malloc(TELNET_MAX_COMMAND_LENGTH); // reserve buffer for Serial/Telnet commands +} + +MyESP::~MyESP() { + end(); +} + +// end +void MyESP::end() { + free(_command); + SerialAndTelnet.end(); + jw.disconnect(); +} + + +// general debug to the telnet or serial channels +void MyESP::myDebug(const char * format, ...) { + va_list args; + va_start(args, format); + char test[1]; + int len = ets_vsnprintf(test, 1, format, args) + 1; + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + va_end(args); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// for flashmemory. Must use PSTR() +void MyESP::myDebug_P(PGM_P format_P, ...) { + char format[strlen_P(format_P) + 1]; + memcpy_P(format, format_P, sizeof(format)); + + va_list args; + va_start(args, format_P); + char test[1]; + int len = ets_vsnprintf(test, 1, format, args) + 1; + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + va_end(args); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// called when WiFi is connected, and used to start MDNS +void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { + if ((code == MESSAGE_CONNECTED) || (code == MESSAGE_ACCESSPOINT_CREATED)) { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + + myDebug_P(PSTR("[WIFI] ----------------------------------------------")); + myDebug_P(PSTR("[WIFI] SSID %s"), WiFi.SSID().c_str()); + myDebug_P(PSTR("[WIFI] CH %d"), WiFi.channel()); + myDebug_P(PSTR("[WIFI] RSSI %d"), WiFi.RSSI()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.macAddress().c_str()); + myDebug_P(PSTR("[WIFI] GW %s"), WiFi.gatewayIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MASK %s"), WiFi.subnetMask().toString().c_str()); + myDebug_P(PSTR("[WIFI] DNS %s"), WiFi.dnsIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] HOST %s"), hostname.c_str()); + myDebug_P(PSTR("[WIFI] ----------------------------------------------")); + + if (WiFi.getMode() & WIFI_AP) { + myDebug_P(PSTR("[WIFI] MODE AP --------------------------------------")); + myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str()); + myDebug_P(PSTR("[WIFI] ----------------------------------------------")); + } + + // start MDNS + if (MDNS.begin((char *)hostname.c_str())) { + myDebug_P(PSTR("[MDNS] OK")); + } else { + myDebug_P(PSTR("[MDNS] FAIL")); + } + + // call any final custom settings + if (_extern_WIFICallbackSet) { + myDebug_P(PSTR("[WIFI] calling custom wifi settings function")); + _extern_WIFICallback(); // call callback to set any custom things + } + } + + if (code == MESSAGE_CONNECTING) { + myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter); + } + + if (code == MESSAGE_CONNECT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter); + } + + if (code == MESSAGE_DISCONNECTED) { + myDebug_P(PSTR("[WIFI] Disconnected")); + } +} + +// received MQTT message +void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { + if (len == 0) + return; + + char message[len + 1]; + strlcpy(message, (char *)payload, len + 1); + + // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); + + // check for our default ones + if ((strcmp(topic, MQTT_HA) == 0) && (strcmp(message, MQTT_TOPIC_START_PAYLOAD) == 0)) { + myDebug_P(PSTR("[MQTT] HA rebooted - restarting device")); + resetESP(); + return; + } + + char s[100]; + snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, MQTT_TOPIC_START); + if (strcmp(topic, s) == 0) { + myDebug_P(PSTR("[MQTT] boottime: %s"), message); + setBoottime(message); + return; + } + + // Send message event to custom service + (_mqtt_callback)(MQTT_MESSAGE_EVENT, topic, message); +} + +// MQTT subscribe +void MyESP::mqttSubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + char s[100]; + snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, topic); + unsigned int packetId = mqttClient.subscribe(s, MQTT_QOS); + myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), s, packetId); + } +} + +// MQTT unsubscribe +void MyESP::mqttUnsubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + char s[100]; + snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, topic); + unsigned int packetId = mqttClient.unsubscribe(s); + myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), s, packetId); + } +} + +// MQTT Publish +void MyESP::mqttPublish(const char * topic, const char * payload) { + char s[500]; + snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, topic); + // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), s, payload); + mqttClient.publish(s, MQTT_QOS, false, payload); +} + +// MQTT onConnect - when a connect is established automatically subscribe to my HA topics +void MyESP::_mqttOnConnect() { + myDebug_P(PSTR("[MQTT] Connected")); + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + +#ifndef NO_HA + // standard subscribes for HA (Home Assistant) + mqttClient.subscribe(MQTT_HA, MQTT_QOS); // to "ha" + mqttSubscribe(MQTT_TOPIC_START); // to home//start + + // send specific start command to HA via MQTT, which returns the boottime + char s[48]; + snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, MQTT_TOPIC_START); + mqttClient.publish(s, MQTT_QOS, false, MQTT_TOPIC_START_PAYLOAD); +#endif + + // call custom + (_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL); +} + +// MQTT setup +void MyESP::_mqtt_setup() { + if (!_mqtt_host) { + myDebug_P(PSTR("[MQTT] disabled")); + } + + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + + mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); }); + + mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { + myDebug_P(PSTR("[MQTT] TCP Disconnected")); + (_mqtt_callback)(MQTT_DISCONNECT_EVENT, NULL, NULL); // call callback with disconnect + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { + myDebug_P(PSTR("[MQTT] Identifier Rejected")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) { + myDebug_P(PSTR("[MQTT] Server unavailable")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) { + myDebug_P(PSTR("[MQTT] Malformed credentials")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { + myDebug_P(PSTR("[MQTT] Not authorized")); + } + }); + + //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.onMessage( + [this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + _mqttOnMessage(topic, payload, len); + }); +} + +// WiFI setup +void MyESP::_wifi_setup() { + jw.setHostname(_app_hostname); // Set WIFI hostname (otherwise it would be ESP-XXXXXX) + jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); + jw.enableAP(false); + jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); + jw.setReconnectTimeout(WIFI_RECONNECT_INTERVAL); + jw.enableAPFallback(true); // AP mode only as fallback, but disabled + jw.enableSTA(true); // Enable STA mode (connecting to a router) + jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm + jw.cleanNetworks(); // Clean existing network configuration + jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network +} + +// MDNS setup +void MyESP::_mdns_setup() { + MDNS.addService("telnet", "tcp", TELNETSPY_PORT); + + // for OTA discovery + MDNS.addServiceTxt("arduino", "tcp", "app_name", (const char *)_app_name); + MDNS.addServiceTxt("arduino", "tcp", "app_version", (const char *)_app_version); + MDNS.addServiceTxt("arduino", "tcp", "mac", WiFi.macAddress()); + { + char buffer[6] = {0}; + itoa(ESP.getFlashChipRealSize() / 1024, buffer, 10); + MDNS.addServiceTxt("arduino", "tcp", "mem_size", (const char *)buffer); + } + { + char buffer[6] = {0}; + itoa(ESP.getFlashChipSize() / 1024, buffer, 10); + MDNS.addServiceTxt("arduino", "tcp", "sdk_size", (const char *)buffer); + } + { + char buffer[6] = {0}; + itoa(ESP.getFreeSketchSpace(), buffer, 10); + MDNS.addServiceTxt("arduino", "tcp", "free_space", (const char *)buffer); + } +} + +// OTA Setup +void MyESP::_ota_setup() { + ArduinoOTA.setPort(OTA_PORT); + ArduinoOTA.setHostname(_app_hostname); + ArduinoOTA.onStart([this]() { myDebug_P(PSTR("[OTA] Start")); }); + ArduinoOTA.onEnd([this]() { myDebug_P(PSTR("[OTA] Done, restarting...")); }); + ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) { + static unsigned int _progOld; + unsigned int _prog = (progress / (total / 100)); + if (_prog != _progOld) { + myDebug_P(PSTR("[OTA] Progress: %u%%\r"), _prog); + _progOld = _prog; + } + }); + + ArduinoOTA.onError([this](ota_error_t error) { + if (error == OTA_AUTH_ERROR) + myDebug_P(PSTR("Auth Failed")); + else if (error == OTA_BEGIN_ERROR) + myDebug_P(PSTR("Begin Failed")); + else if (error == OTA_CONNECT_ERROR) + myDebug_P(PSTR("Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) + myDebug_P(PSTR("Receive Failed")); + else if (error == OTA_END_ERROR) + myDebug_P(PSTR("End Failed")); + }); + + ArduinoOTA.begin(); +} + +// sets boottime +void MyESP::setBoottime(char * boottime) { + if (_boottime) { + free(_boottime); + } + _boottime = strdup(boottime); +} + +// returns boottime +char * MyESP::getBoottime() { + return _boottime; +} + +// Set callback of sketch function to process project messages +void MyESP::consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)()) { + _helpProjectCmds = cmds; // command list + _helpProjectCmds_count = count; // number of commands + _consoleCallbackProjectCmds = callback; // external function to handle commands +} + +void MyESP::_telnetConnected() { + myDebug_P(PSTR("[TELNET] Telnet connection established")); + _consoleShowHelp(); // Show the initial message +} + +void MyESP::_telnetDisconnected() { + myDebug_P(PSTR("[TELNET] Telnet connection closed")); +} + +// Initialize the telnet server +void MyESP::_telnet_setup() { + SerialAndTelnet.setWelcomeMsg(""); + SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); }); + SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); }); + SerialAndTelnet.begin(115200); + SerialAndTelnet.setDebugOutput(false); + +#ifndef DEBUG_SUPPORT + SerialAndTelnet.setSerial(NULL); +#endif + + // init command buffer for console commands + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); +} + +// Show help of commands +void MyESP::_consoleShowHelp() { + String help = "\n\r**********************************************\n\r* Remote Telnet Command Center & Log Monitor " + "*\n\r**********************************************\n\r"; + help += "* Device hostname: " + WiFi.hostname() + "\tIP: " + WiFi.localIP().toString() + "\tMAC address: " + WiFi.macAddress() + "\n\r"; + help += "* Connected to WiFi AP: " + WiFi.SSID() + "\n\r"; + help += "* Boot time: "; + help.concat(_boottime); + help += "\n\r* "; + help.concat(_app_name); + help += " Version "; + help.concat(_app_version); + help += "\n\r* Free RAM: "; + help.concat(ESP.getFreeHeap()); + help += " bytes\n\r"; +#ifdef DEBUG_SUPPORT + help += "* !! in DEBUG_SUPPORT mode !!\n\r"; +#endif + help += "*\n\r* Commands:\n\r* ?=this help, CTRL-D=quit, $=show free memory, !=reboot ESP, &=suspend all messages\n\r"; + + // print custom commands if available + if (_consoleCallbackProjectCmds) { + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + help += FPSTR("* "); + help += FPSTR(_helpProjectCmds[i].key); + for (int j = 0; j < (8 - strlen(_helpProjectCmds[i].key)); j++) { // padding + help += FPSTR(" "); + } + help += FPSTR(_helpProjectCmds[i].description); + help += FPSTR("\n\r"); + } + } + + SerialAndTelnet.println(help.c_str()); +} + +// reset / restart +void MyESP::resetESP() { + myDebug_P(PSTR("* Reboot ESP...")); + end(); + +#if defined(ARDUINO_ARCH_ESP32) + ESP.reset(); // for ESP8266 only +#else + ESP.restart(); +#endif +} + +// Get last command received +char * MyESP::consoleGetLastCommand() { + return _command; +} + +// Process user command over telnet +void MyESP::consoleProcessCommand() { + uint8_t cmd = _command[0]; + + // Process the command + if (cmd == '?') { + _consoleShowHelp(); // Show help + } else if (cmd == '$') { + myDebug("* Free RAM (bytes): %d", ESP.getFreeHeap()); + } else if (cmd == '!') { + resetESP(); + } else if (cmd == '&') { + _verboseMessages = !_verboseMessages; // toggle + myDebug("Suspend all messages is %s", _verboseMessages ? "disabled" : "enabled"); + } else { + // custom Project commands + if (_consoleCallbackProjectCmds) { + _consoleCallbackProjectCmds(); + } + } + + if (!_verboseMessages) { + myDebug("Warning, all log messages have been supsended. Use & to re-enable."); + } +} + +// sends a MQTT notification message to Home Assistant +void MyESP::sendHANotification(const char * message) { + char payload[48]; + snprintf(payload, sizeof(payload), "%s : %s", _app_hostname, message); + myDebug_P(PSTR("[MQTT] Sending HA notification %s"), payload); + mqttClient.publish(MQTT_NOTIFICATION, MQTT_QOS, false, payload); +} + +// send specific command to HA via MQTT +// format is: home//command with payload +void MyESP::sendHACommand(const char * cmd) { + myDebug_P(PSTR("[MQTT] Sending HA command %s"), cmd); + char topic[48]; + snprintf(topic, sizeof(topic), "%s%s/%s", MQTT_BASE, _app_hostname, MQTT_TOPIC_COMMAND); + mqttClient.publish(topic, MQTT_QOS, false, cmd); +} + +// handler for Telnet +void MyESP::_telnetHandle() { + SerialAndTelnet.handle(); + + char last = ' '; // To avoid processing double "\r\n" + + while (SerialAndTelnet.available()) { + char character = SerialAndTelnet.read(); // Get character + + // check for ctrl-D + if ((character == 0xEC) || (character == 0x04)) { + SerialAndTelnet.disconnectClient(); + } + + // if we reached our buffer limit, send what we have + if (strlen(_command) >= TELNET_MAX_COMMAND_LENGTH) { + consoleProcessCommand(); // Process the command + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); // reset for next command + } + + // Check for newline (CR or LF) + if (_isCRLF(character) == true) { + if (_isCRLF(last) == false) { + if (strlen(_command) > 0) { + consoleProcessCommand(); // Process the command + } + } + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); // reset for next command + + } else if (isPrintable(character)) { + // Concat char to end of buffer + uint16_t len = strlen(_command); + _command[len] = character; + _command[len + 1] = '\0'; + } + last = character; // remember last char + } +} + +// sets a custom function to run when wifi is started +void MyESP::setWIFICallback(void (*callback)()) { + _extern_WIFICallback = callback; + _extern_WIFICallbackSet = true; +} + +// the mqtt callback for after connecting to subscribe and process incoming messages +void MyESP::setMQTTCallback(mqtt_callback_f callback) { + _mqtt_callback = callback; +} + +// Is CR or LF ? +bool MyESP::_isCRLF(char character) { + return (character == '\r' || character == '\n'); +} + +// ensure we have a connection to MQTT broker +void MyESP::_mqttConnect() { + if (!_mqtt_host || mqttClient.connected() || (WiFi.status() != WL_CONNECTED)) { + return; + } + + // Check reconnect interval + static unsigned long last = 0; + if (millis() - last < _mqtt_reconnect_delay) + return; + last = millis(); + + // Increase the reconnect delay + _mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP; + if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) { + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; + } + + mqttClient.setServer(_mqtt_host, MQTT_PORT); + mqttClient.setClientId(_app_hostname); + + if (_mqtt_username && _mqtt_password) { + myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s & its password"), _mqtt_username); + mqttClient.setCredentials(_mqtt_username, _mqtt_password); + } else { + myDebug_P(PSTR("[MQTT] Connecting to MQTT...")); + } + + // Connect to the MQTT broker + mqttClient.connect(); +} + +// Setup everything we need +void MyESP::setup(char * app_hostname, + char * app_name, + char * app_version, + char * wifi_ssid, + char * wifi_password, + char * mqtt_host, + char * mqtt_username, + char * mqtt_password) { + // get general params first + _app_hostname = strdup(app_hostname); + _app_name = strdup(app_name); + _app_version = strdup(app_version); + + // Check SSID too long or missing + if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > 31) { + _wifi_ssid = NULL; + } else { + _wifi_ssid = strdup(wifi_ssid); + } + + // Check PASS too long + if (wifi_password && strlen(wifi_password) > 63) { + _wifi_password = NULL; + } else { + _wifi_password = strdup(wifi_password); + } + + // can be empty + if (!mqtt_host || *mqtt_host == 0x00) { + _mqtt_host = NULL; + } else { + _mqtt_host = strdup(mqtt_host); + } + + // mqtt username and password can be empty + if (!mqtt_username || *mqtt_username == 0x00) { + _mqtt_username = NULL; + } else { + _mqtt_username = strdup(mqtt_username); + } + + // can be empty + if (!mqtt_password || *mqtt_password == 0x00) { + _mqtt_password = NULL; + } else { + _mqtt_password = strdup(mqtt_password); + } + + // call setup of the services... + _telnet_setup(); // Telnet setup + _wifi_setup(); // WIFI setup + _mqtt_setup(); // MQTT Setup + _mdns_setup(); // MDNS setup + _ota_setup(); // OTA setup +} + +/* + * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc + */ +void MyESP::loop() { + jw.loop(); // WiFi + _telnetHandle(); // Telnet/Debugger + ArduinoOTA.handle(); // OTA + _mqttConnect(); // MQTT + + yield(); // ...and breath +} + +MyESP myESP; diff --git a/lib/myESP/MyESP.h b/lib/myESP/MyESP.h new file mode 100644 index 000000000..895e50fc6 --- /dev/null +++ b/lib/myESP/MyESP.h @@ -0,0 +1,169 @@ +/* + * MyEsp.h + * + * Paul Derbyshire - December 2018 + */ + +#pragma once + +#ifndef MyEMS_h +#define MyEMS_h + +#include +#include +#include // https://github.com/marvinroger/async-mqtt-client +#include +#include // https://github.com/me-no-dev/ESPAsyncTCP +#include // https://github.com/xoseperez/justwifi +#include // modified from https://github.com/yasheena/telnetspy + +#if defined(ARDUINO_ARCH_ESP32) +#include +#else +#include +#endif + +// WIFI +#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms +#define WIFI_RECONNECT_INTERVAL 60000 // If could not connect to WIFI, retry after this time in ms + +// OTA +#define OTA_PORT 8266 // OTA port + +// MQTT +#define MQTT_BASE "home/" +#define MQTT_NOTIFICATION MQTT_BASE "notification" +#define MQTT_TOPIC_COMMAND "command" +#define MQTT_TOPIC_START "start" +#define MQTT_TOPIC_START_PAYLOAD "start" +#define MQTT_HA MQTT_BASE "ha" +#define MQTT_PORT 1883 // MQTT port +#define MQTT_QOS 1 +#define MQTT_RECONNECT_DELAY_MIN 5000 // Try to reconnect in 5 seconds upon disconnection +#define MQTT_RECONNECT_DELAY_STEP 5000 // Increase the reconnect delay in 5 seconds after each failed attempt +#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most +// Internal MQTT events +#define MQTT_CONNECT_EVENT 0 +#define MQTT_DISCONNECT_EVENT 1 +#define MQTT_MESSAGE_EVENT 2 + +// Telnet +#define TELNET_MAX_COMMAND_LENGTH 80 // length of a command +#define COLOR_RESET "\x1B[0m" +#define COLOR_BLACK "\x1B[0;30m" +#define COLOR_RED "\x1B[0;31m" +#define COLOR_GREEN "\x1B[0;32m" +#define COLOR_YELLOW "\x1B[0;33m" +#define COLOR_BLUE "\x1B[0;34m" +#define COLOR_MAGENTA "\x1B[0;35m" +#define COLOR_CYAN "\x1B[0;36m" +#define COLOR_WHITE "\x1B[0;37m" + +typedef struct { + char key[10]; + char description[400]; +} command_t; + +typedef std::function mqtt_callback_f; + +// calculates size of an 2d array at compile time +template +constexpr size_t ArraySize(T (&)[N]) { + return N; +} + +// class definition +class MyESP { + public: + MyESP(); + ~MyESP(); + + // wifi + void setWIFICallback(void (*callback)()); + void setMQTTCallback(mqtt_callback_f callback); + + // ha + void sendHACommand(const char * cmd); + void sendHANotification(const char * message); + + // mqtt + void mqttSubscribe(const char * topic); + void mqttUnsubscribe(const char * topic); + void mqttPublish(const char * topic, const char * payload); + + // debug & telnet + void myDebug(const char * format, ...); + void myDebug_P(PGM_P format_P, ...); + void consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)()); + char * consoleGetLastCommand(); + void consoleProcessCommand(); + + void end(); + void loop(); + void setup(char * app_hostname, + char * app_name, + char * app_version, + char * wifi_ssid, + char * wifi_password, + char * mqtt_host, + char * mqtt_username, + char * mqtt_password); + + char * getBoottime(); + void setBoottime(char * boottime); + void resetESP(); + + private: + // mqtt + AsyncMqttClient mqttClient; + unsigned long _mqtt_reconnect_delay; + void _mqttOnMessage(char * topic, char * payload, size_t len); + void _mqttConnect(); + void _mqtt_setup(); + mqtt_callback_f _mqtt_callback; + void _mqttOnConnect(); + void _sendStart(); + char * _mqtt_host; + char * _mqtt_username; + char * _mqtt_password; + char * _boottime; + + // wifi + DNSServer dnsServer; // For Access Point (AP) support + void _wifiCallback(justwifi_messages_t code, char * parameter); + void _wifi_setup(); + void (*_extern_WIFICallback)(); + bool _extern_WIFICallbackSet; + char * _wifi_ssid; + char * _wifi_password; + + // mdns + void _mdns_setup(); + + // ota + void _ota_setup(); + + // telnet & debug + TelnetSpy SerialAndTelnet; + void _telnetConnected(); + void _telnetDisconnected(); + void _telnetHandle(); + void _telnet_setup(); + char * _command; // the input command from either Serial or Telnet + command_t * _helpProjectCmds; // Help of commands setted by project + uint8_t _helpProjectCmds_count; // # available commands + void _consoleShowHelp(); + void (*_consoleCallbackProjectCmds)(); // Callable for projects commands + void _consoleProcessCommand(); + bool _isCRLF(char character); + bool _verboseMessages; + + // general + char * _app_hostname; + char * _app_name; + char * _app_version; +}; + +extern MyESP myESP; + +#endif diff --git a/platformio.ini-example b/platformio.ini-example index 18ef52170..6e93508f8 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -1,19 +1,20 @@ [platformio] ; change this for your ESP8266 device -env_default = nodemcuv2 -; env_default = d1_mini +; env_default = nodemcuv2 +env_default = d1_mini [common] platform = espressif8266 -; optional flags are -DUSE_LED -DSHOWER_TEST -DUSE_SERIAL -build_flags = -g -w -DMQTT_MAX_PACKET_SIZE=400 +flash_mode = dout +; optional flags are -DNO_LED -DDEBUG_SUPPORT +build_flags = -g -w build_flags_custom = '-DWIFI_SSID="my_ssid"' '-DWIFI_PASSWORD="my_password"' '-DMQTT_IP="my_broker_ip"' '-DMQTT_USER="my_broker_username"' '-DMQTT_PASS="my_broker_password"' lib_deps = - Time - PubSubClient - ArduinoJson CRC32 CircularBuffer + JustWifi + AsyncMqttClient + ArduinoJson [env:nodemcuv2] board = nodemcuv2 @@ -21,12 +22,13 @@ platform = ${common.platform} framework = arduino lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} ${common.build_flags_custom} +board_build.flash_mode = ${common.flash_mode} upload_speed = 921600 monitor_speed = 115200 ; comment out next line if using USB and not OTA -upload_port = "boiler." +upload_port = "boiler" ; examples.... -;upload_port = "boiler" +;upload_port = "boiler." ;upload_port = "boiler.local" ;upload_port = 10.10.10.6 @@ -36,12 +38,13 @@ platform = ${common.platform} framework = arduino lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} ${common.build_flags_custom} +board_build.flash_mode = ${common.flash_mode} upload_speed = 921600 monitor_speed = 115200 ; comment out next line if using USB and not OTA -upload_port = "boiler." +upload_port = "boiler" ; examples.... -;upload_port = "boiler" +;upload_port = "boiler." ;upload_port = "boiler.local" ;upload_port = 10.10.10.6 diff --git a/src/ESPHelper.cpp b/src/ESPHelper.cpp deleted file mode 100644 index 55284d2e0..000000000 --- a/src/ESPHelper.cpp +++ /dev/null @@ -1,895 +0,0 @@ -/* - Based off : - 1) ESPHelper.cpp - Copyright (c) 2017 ItKindaWorks Inc All right reserved. github.com/ItKindaWorks - 2) https://github.com/JoaoLopesF/ESP8266-RemoteDebug-Telnet - -*/ - -#include "ESPHelper.h" - -WiFiServer telnetServer(TELNET_PORT); - -//initializer with single netInfo network -ESPHelper::ESPHelper(netInfo * startingNet) { - //disconnect from and previous wifi networks - WiFi.softAPdisconnect(); - WiFi.disconnect(); - - //setup current network information - _currentNet = *startingNet; - - //validate various bits of network/MQTT info - - //network pass - if (_currentNet.pass[0] == '\0') { - _passSet = false; - } else { - _passSet = true; - } - - //ssid - if (_currentNet.ssid[0] == '\0') { - _ssidSet = false; - } else { - _ssidSet = true; - } - - //mqtt host - if (_currentNet.mqttHost[0] == '\0') { - _mqttSet = false; - } else { - _mqttSet = true; - } - - //mqtt port - if (_currentNet.mqttPort == 0) { - _currentNet.mqttPort = 1883; - } - - //mqtt username - if (_currentNet.mqttUser[0] == '\0') { - _mqttUserSet = false; - } else { - _mqttUserSet = true; - } - - //mqtt password - if (_currentNet.mqttPass[0] == '\0') { - _mqttPassSet = false; - } else { - _mqttPassSet = true; - } - - //disable hopping on single network - _hoppingAllowed = false; - - //disable ota by default - _useOTA = false; -} - -//start the wifi & mqtt systems and attempt connection (currently blocking) -//true on: parameter check validated -//false on: parameter check failed -bool ESPHelper::begin(const char * hostname, const char * app_name, const char * app_version) { -#ifdef USE_SERIAL1 - Serial1.begin(115200); - Serial1.setDebugOutput(true); -#endif - -#ifdef USE_SERIAL - Serial.begin(115200); - Serial.setDebugOutput(true); -#endif - - // set hostname first - strcpy(_hostname, hostname); - OTA_enable(); - - strcpy(_app_name, app_name); // app name - strcpy(_app_version, app_version); // app version - - - setBoottime(""); - - if (_ssidSet) { - strcpy(_clientName, hostname); - - /* - // Generate client name based on MAC address and last 8 bits of microsecond counter - - _clientName += "esp-"; - uint8_t mac[6]; - WiFi.macAddress(mac); - _clientName += macToStr(mac); - */ - -// set hostname -// can ping by or on Windows10 it's . -#if defined(ESP8266) - WiFi.hostname(_hostname); -#elif defined(ESP32) - WiFi.setHostname(_hostname); -#endif - - //set the wifi mode to station and begin the wifi (connect using either ssid or ssid/pass) - WiFi.mode(WIFI_STA); - if (_passSet) { - WiFi.begin(_currentNet.ssid, _currentNet.pass); - } else { - WiFi.begin(_currentNet.ssid); - } - - //as long as an mqtt ip has been set create an instance of PubSub for client - if (_mqttSet) { - //make mqtt client use either the secure or non-secure wifi client depending on the setting - if (_useSecureClient) { - client = PubSubClient(_currentNet.mqttHost, _currentNet.mqttPort, wifiClientSecure); - } else { - client = PubSubClient(_currentNet.mqttHost, _currentNet.mqttPort, wifiClient); - } - - //set the mqtt message callback if needed - if (_mqttCallbackSet) { - client.setCallback(_mqttCallback); - } - } - - //define a dummy instance of mqtt so that it is instantiated if no mqtt ip is set - else { - //make mqtt client use either the secure or non-secure wifi client depending on the setting - //(this shouldnt be needed if making a dummy connection since the idea would be that there wont be mqtt in this case) - if (_useSecureClient) { - client = PubSubClient("192.168.1.255", _currentNet.mqttPort, wifiClientSecure); - } else { - client = PubSubClient("192.168.1.255", _currentNet.mqttPort, wifiClient); - } - } - - //ota event handlers - ArduinoOTA.onStart([]() { /* ota start code */ }); - ArduinoOTA.onEnd([]() { - //on ota end we disconnect from wifi cleanly before restarting. - WiFi.softAPdisconnect(); - WiFi.disconnect(); - uint8_t timeout = 0; - //max timeout of 2seconds before just dropping out and restarting - while (WiFi.status() != WL_DISCONNECTED && timeout < 200) { - timeout++; - } - }); - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { /* ota progress code */ }); - ArduinoOTA.onError([](ota_error_t error) { /* ota error code */ }); - - //initially attempt to connect to wifi when we begin (but only block for 2 seconds before timing out) - uint8_t timeout = 0; //counter for begin connection attempts - while (((!client.connected() && _mqttSet) || WiFi.status() != WL_CONNECTED) - && timeout < 200) { //max 2 sec before timeout - reconnect(); - timeout++; - } - - //attempt to start ota if needed - OTA_begin(); - - // Initialize the telnet server - telnetServer.begin(); - telnetServer.setNoDelay(true); - - // init command buffer for console commands - memset(_command, 0, sizeof(_command)); - - consoleShowHelp(); // show this at bootup - - // mark the system as started and return - _hasBegun = true; - - return true; - } - - //if no ssid was set even then dont try to begin and return false - return false; -} - -//end the instance of ESPHelper (shutdown wifi, ota, mqtt) -void ESPHelper::end() { - // Stop telnet Client & Server - if (telnetClient && telnetClient.connected()) { - telnetClient.stop(); - } - - // Stop server - telnetServer.stop(); - - OTA_disable(); - WiFi.softAPdisconnect(); - WiFi.disconnect(); - - uint8_t timeout = 0; - while (WiFi.status() != WL_DISCONNECTED && timeout < 200) { - timeout++; - } - -#ifdef USE_SERIAL - Serial.flush(); -#endif - -#ifdef USE_SERIAL1 - Serial1.flush(); -#endif -} - -//main loop - should be called as often as possible - handles wifi/mqtt connection and mqtt handler -//true on: network/server connected -//false on: network or server disconnected -int ESPHelper::loop() { - if (_ssidSet) { - //check for good connections and attempt a reconnect if needed - if (((_mqttSet && !client.connected()) || setConnectionStatus() < WIFI_ONLY) && _connectionStatus != BROADCAST) { - reconnect(); - } - - //run the wifi loop as long as the connection status is at a minimum of BROADCAST - if (_connectionStatus >= BROADCAST) { - //run the MQTT loop if we have a full connection - if (_connectionStatus == FULL_CONNECTION) { - client.loop(); - } - - //check for whether we want to use OTA and whether the system is running - if (_useOTA && _OTArunning) { - ArduinoOTA.handle(); - } - - //if we want to use OTA but its not running yet, start it up. - else if (_useOTA && !_OTArunning) { - OTA_begin(); - ArduinoOTA.handle(); - } - - // do the telnet stuff - consoleHandle(); - - return _connectionStatus; - } - } - - //return -1 for no connection because of bad network info - return -1; -} - -//subscribe to a specific topic (does not add to topic list) -//true on: subscription success -//false on: subscription failed (either from PubSub lib or network is disconnected) -bool ESPHelper::subscribe(const char * topic, uint8_t qos) { - if (_connectionStatus == FULL_CONNECTION) { - //set the return value to the output of subscribe - bool returnVal = client.subscribe(topic, qos); - - //loop mqtt client - client.loop(); - return returnVal; - } - - //if not fully connected return false - else { - return false; - } -} - -//add a topic to the list of subscriptions and attempt to subscribe to the topic on the spot -//true on: subscription added to list (does not guarantee that the topic was subscribed to, only that it was added to the list) -//false on: subscription not added to list -bool ESPHelper::addSubscription(const char * topic) { - //default return value is false - bool subscribed = false; - - //loop through finding the next available slot for a subscription and add it - for (uint8_t i = 0; i < MAX_SUBSCRIPTIONS; i++) { - if (_subscriptions[i].isUsed == false) { - _subscriptions[i].topic = topic; - _subscriptions[i].isUsed = true; - subscribed = true; - break; - } - } - - //if added to the list, subscribe to the topic - if (subscribed) { - subscribe(topic, _qos); - } - - return subscribed; -} - -//loops through list of subscriptions and attempts to subscribe to all topics -void ESPHelper::resubscribe() { - for (uint8_t i = 0; i < MAX_SUBSCRIPTIONS; i++) { - if (_subscriptions[i].isUsed) { - subscribe(_subscriptions[i].topic, _qos); - yield(); - } - } -} - -//manually unsubscribes from a topic (This is basically just a wrapper for the pubsubclient function) -bool ESPHelper::unsubscribe(const char * topic) { - return client.unsubscribe(topic); -} - -//publish to a specified topic -void ESPHelper::publish(const char * topic, const char * payload) { - if (_mqttSet) { - publish(topic, payload, false); - } -} - -//publish to a specified topic with a given retain level -void ESPHelper::publish(const char * topic, const char * payload, bool retain) { - client.publish(topic, payload, retain); -} - -//set the callback function for MQTT -void ESPHelper::setMQTTCallback(MQTT_CALLBACK_SIGNATURE) { - _mqttCallback = callback; - - //only set the callback if using mqtt AND the system has already been started. Otherwise just save it for later - if (_hasBegun && _mqttSet) { - client.setCallback(_mqttCallback); - } - _mqttCallbackSet = true; -} - -//sets a custom function to run when connection to wifi is established -void ESPHelper::setWifiCallback(void (*callback)()) { - _wifiCallback = callback; - _wifiCallbackSet = true; -} - -//sets a custom function to run when telnet is started -void ESPHelper::setInitCallback(void (*callback)()) { - _initCallback = callback; - _initCallbackSet = true; -} - -//attempts to connect to wifi & mqtt server if not connected -void ESPHelper::reconnect() { - static uint8_t tryCount = 0; - - if (_connectionStatus != BROADCAST && setConnectionStatus() != FULL_CONNECTION) { - logger(LOG_CONSOLE, "Attempting WiFi Connection..."); - //attempt to connect to the wifi if connection is lost - if (WiFi.status() != WL_CONNECTED) { - _connectionStatus = NO_CONNECTION; - - //increment try count each time it cannot connect (this is used to determine when to hop to a new network) - tryCount++; - if (tryCount == 20) { - //change networks (if possible) when we have tried to connect 20 times and failed - changeNetwork(); - tryCount = 0; - return; - } - } - - // make sure we are connected to WIFI before attempting to reconnect to MQTT - //----note---- maybe want to reset tryCount whenever we succeed at getting wifi connection? - if (WiFi.status() == WL_CONNECTED) { - //if the wifi previously wasnt connected but now is, run the callback - if (_connectionStatus < WIFI_ONLY && _wifiCallbackSet) { - _wifiCallback(); - } - - logger(LOG_CONSOLE, "---WiFi Connected!---"); - _connectionStatus = WIFI_ONLY; - - //attempt to connect to mqtt when we finally get connected to WiFi - if (_mqttSet) { - static uint8_t timeout = 0; //allow a max of 5 mqtt connection attempts before timing out - if (!client.connected() && timeout < 5) { - logger(LOG_CONSOLE, "Attempting MQTT connection..."); - - uint8_t connected = 0; - - //connect to mqtt with user/pass - if (_mqttUserSet) { - connected = client.connect(_clientName, _currentNet.mqttUser, _currentNet.mqttPass); - } - - //connect to mqtt without credentials - else { - connected = client.connect(_clientName); - } - - //if connected, subscribe to the topic(s) we want to be notified about - if (connected) { - logger(LOG_CONSOLE, " -- Connected"); - - //if using https, verify the fingerprint of the server before setting full connection (return on fail) - // removing this as not supported with ESP32, see https://github.com/espressif/arduino-esp32/issues/278 - /* - if (wifiClientSecure.verify(_fingerprint, - _currentNet.mqttHost)) { - logger(LOG_CONSOLE, - "Certificate Matches - SUCCESS\n"); - } else { - logger(LOG_CONSOLE, - "Certificate Doesn't Match - FAIL\n"); - return; - } - } - */ - - _connectionStatus = FULL_CONNECTION; - resubscribe(); - timeout = 0; - } else { - logger(LOG_CONSOLE, " -- Failed\n"); - } - timeout++; - } - - //if we still cant connect to mqtt after 10 attempts increment the try count - if (timeout >= 5 && !client.connected()) { - timeout = 0; - tryCount++; - if (tryCount == 20) { - changeNetwork(); - tryCount = 0; - return; - } - } - } - } - - //reset the reconnect metro - //reconnectMetro.reset(); - } -} - -uint8_t ESPHelper::setConnectionStatus() { - //assume no connection - uint8_t returnVal = NO_CONNECTION; - - //make sure were not in broadcast mode - if (_connectionStatus != BROADCAST) { - //if connected to wifi set the mode to wifi only and run the callback if needed - if (WiFi.status() == WL_CONNECTED) { - if (_connectionStatus < WIFI_ONLY - && _wifiCallbackSet) { //if the wifi previously wasn't connected but now is, run the callback - _wifiCallback(); - } - returnVal = WIFI_ONLY; - - //if mqtt is connected as well then set the status to full connection - if (client.connected()) { - returnVal = FULL_CONNECTION; - } - } - } - - else { - returnVal = BROADCAST; - } - - //set the connection status and return - _connectionStatus = returnVal; - return returnVal; -} - -//changes the current network settings to the next listed network if network hopping is allowed -void ESPHelper::changeNetwork() { - //only attempt to change networks if hopping is allowed - if (_hoppingAllowed) { - //change the index/reset to 0 if we've hit the last network setting - _currentIndex++; - if (_currentIndex >= _netCount) { - _currentIndex = 0; - } - - //set the current netlist to the new network - _currentNet = *_netList[_currentIndex]; - - //verify various bits of network info - - //network password - if (_currentNet.pass[0] == '\0') { - _passSet = false; - } else { - _passSet = true; - } - - //ssid - if (_currentNet.ssid[0] == '\0') { - _ssidSet = false; - } else { - _ssidSet = true; - } - - //mqtt host - if (_currentNet.mqttHost[0] == '\0') { - _mqttSet = false; - } else { - _mqttSet = true; - } - - //mqtt username - if (_currentNet.mqttUser[0] == '\0') { - _mqttUserSet = false; - } else { - _mqttUserSet = true; - } - - //mqtt password - if (_currentNet.mqttPass[0] == '\0') { - _mqttPassSet = false; - } else { - _mqttPassSet = true; - } - - printf("Trying next network: %s\n", _currentNet.ssid); - - //update the network connection - updateNetwork(); - } -} - -void ESPHelper::updateNetwork() { - logger(LOG_CONSOLE, "\tDisconnecting from WiFi"); - WiFi.disconnect(); - logger(LOG_CONSOLE, "\tAttempting to begin on new network..."); - - //set the wifi mode - WiFi.mode(WIFI_STA); - - //connect to the network - if (_passSet && _ssidSet) { - WiFi.begin(_currentNet.ssid, _currentNet.pass); - } else if (_ssidSet) { - WiFi.begin(_currentNet.ssid); - } else { - WiFi.begin("NO_SSID_SET"); - } - - logger(LOG_CONSOLE, "\tSetting new MQTT server"); - //setup the mqtt broker info - if (_mqttSet) { - client.setServer(_currentNet.mqttHost, _currentNet.mqttPort); - } else { - client.setServer("192.168.1.3", 1883); - } - - logger(LOG_CONSOLE, "\tDone - Ready for next reconnect attempt"); -} - -//enable use of OTA updates -void ESPHelper::OTA_enable() { - _useOTA = true; - ArduinoOTA.setHostname(_hostname); - OTA_begin(); -} - -//begin the OTA subsystem but with a check for connectivity and enabled use of OTA -void ESPHelper::OTA_begin() { - if (_connectionStatus >= BROADCAST && _useOTA) { - ArduinoOTA.begin(); - _OTArunning = true; - } -} - -//disable use of OTA updates -void ESPHelper::OTA_disable() { - _useOTA = false; - _OTArunning = false; -} - -// Is CR or LF ? -bool ESPHelper::isCRLF(char character) { - return (character == '\r' || character == '\n'); -} - -// handler for Telnet -void ESPHelper::consoleHandle() { - // look for Client - if (telnetServer.hasClient()) { - if (telnetClient && telnetClient.connected()) { - // Verify if the IP is same than actual connection - WiFiClient newClient; - newClient = telnetServer.available(); - String ip = newClient.remoteIP().toString(); - - if (ip == telnetClient.remoteIP().toString()) { - // Reconnect - telnetClient.stop(); - telnetClient = newClient; - } else { - // Desconnect (not allow more than one connection) - newClient.stop(); - return; - } - } else { - // New TCP client - telnetClient = telnetServer.available(); - } - - if (!telnetClient) { // No client yet ??? - return; - } - - // Set client - telnetClient.setNoDelay(true); // faster - telnetClient.flush(); // clear input buffer, to prevent strange characters - - _lastTimeCommand = millis(); // To mark time for inactivity - - // Show the initial message - consoleShowHelp(); - - _initCallback(); // call callback to set any custom things - - // Empty buffer - while (telnetClient.available()) { - telnetClient.read(); - } - } - - // Is client connected ? (to reduce overhead in active) - _telnetConnected = (telnetClient && telnetClient.connected()); - - // Get command over telnet - if (_telnetConnected) { - char last = ' '; // To avoid processing double "\r\n" - - while (telnetClient.available()) { // get data from Client - - // Get character - char character = telnetClient.read(); - - // Newline (CR or LF) - once one time if (\r\n) - if (isCRLF(character) == true) { - if (isCRLF(last) == false) { - // Process the command - if (strlen(_command) > 0) { - consoleProcessCommand(); - } - } - // reset for next command - memset(_command, 0, sizeof(_command)); - } else if (isPrintable(character)) { - // Concat char to end of buffer - uint16_t len = strlen(_command); - _command[len] = character; - _command[len + 1] = '\0'; - } - - // Last char - last = character; - } - - // Inactivity - close connection if not received commands from user in telnet to reduce overheads - if ((millis() - _lastTimeCommand) > MAX_TIME_INACTIVE) { - telnetClient.println("* Closing telnet session due to inactivity"); - telnetClient.flush(); - telnetClient.stop(); - _telnetConnected = false; - } - } -} - -// Set callback of sketch function to process project messages -void ESPHelper::consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)()) { - _helpProjectCmds = cmds; // command list - _helpProjectCmds_count = count; // number of commands - _consoleCallbackProjectCmds = callback; // external function to handle commands -} - -// Set bootime received as a string from HA -void ESPHelper::setBoottime(const char * boottime) { - strcpy(_boottime, boottime); -} - -// overrides the write call to print to the telnet connection -size_t ESPHelper::write(uint8_t character) { - if (!_verboseMessages) - return 0; - - //static uint32_t elapsed = 0; - - // If start of a new line, initiate a new string buffer with time counter as a prefix - if (_newLine) { - unsigned long upt = millis(); - sprintf(bufferPrint, - "(%s%02d:%02d:%02d%s) ", - COLOR_CYAN, - (uint8_t)((upt / (1000 * 60 * 60)) % 24), - (uint8_t)((upt / (1000 * 60)) % 60), - (uint8_t)((upt / 1000) % 60), - COLOR_RESET); - _newLine = false; - } - - // Print ? - bool doPrint = false; - - // New line ? - if (character == '\n') { - _newLine = true; - doPrint = true; - } else if (strlen(bufferPrint) == BUFFER_PRINT - 1) { // Limit of buffer - doPrint = true; - } - - // Add character to telnet buffer - uint16_t len = strlen(bufferPrint); - bufferPrint[len] = character; - - if (_newLine) { - // add additional \r for windows - bufferPrint[++len] = '\r'; - } - - // terminate string - bufferPrint[++len] = '\0'; - - // Send the characters buffered by print.h - if (doPrint) { - if (_telnetConnected) { - telnetClient.print(bufferPrint); - } - -// echo to Serial if enabled -#ifdef USE_SERIAL - Serial.print(bufferPrint); -#endif - -#ifdef USE_SERIAL1 - Serial1.print(bufferPrint); -#endif - - // Empty the buffer - bufferPrint[0] = '\0'; - } - - return len + 1; -} - -// Show help of commands -void ESPHelper::consoleShowHelp() { - String help = "**********************************************\n\r* Remote Telnet Command Center & Log Monitor " - "*\n\r**********************************************\n\r"; - help += "* Device hostname: " + WiFi.hostname() + "\tIP: " + WiFi.localIP().toString() - + "\tMAC address: " + WiFi.macAddress() + "\n\r"; - help += "* Connected to WiFi AP: " + WiFi.SSID() + "\n\r"; - help += "* Boot time: "; - help.concat(_boottime); - help += "\n\r* "; - help.concat(_app_name); - help += " Version "; - help.concat(_app_version); - help += "\n\r* Free RAM: "; - help.concat(ESP.getFreeHeap()); - help += " bytes\n\r"; - help += "*\n\r* Commands:\n\r* ?=this help, q=quit telnet, $=show free memory, !=reboot, &=suspend all " - "notifications\n\r"; - - char s[100]; - - // print custom commands if available - if (_consoleCallbackProjectCmds) { - for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { - help += FPSTR("* "); - help += FPSTR(_helpProjectCmds[i].key); - for (int j = 0; j < (8 - strlen(_helpProjectCmds[i].key)); j++) { // padding - help += FPSTR(" "); - } - help += FPSTR(_helpProjectCmds[i].description); - help += FPSTR("\n\r"); - } - } - - telnetClient.print(help); - -#ifdef USE_SERIAL - Serial.print(help); -#endif - -#ifdef USE_SERIAL1 - Serial1.print(help); -#endif -} - -// reset / restart -void ESPHelper::resetESP() { - telnetClient.println("* Reboot ESP..."); - telnetClient.flush(); - telnetClient.stop(); - // end(); - - // Reset - ESP.restart(); - // ESP.reset(); // for ESP8266 only -} - -// Get last command received -char * ESPHelper::consoleGetLastCommand() { - return _command; -} - -// Process user command over telnet -void ESPHelper::consoleProcessCommand() { - // Set time of last command received - _lastTimeCommand = millis(); - uint8_t cmd = _command[0]; - - if (!_verboseMessages) { - telnetClient.println("Warning, all messages are supsended. Use & to enable."); - } - - // Process the command - if (cmd == '?') { - consoleShowHelp(); // Show help - } else if (cmd == 'q') { // quit - telnetClient.println("* Closing telnet connection..."); - telnetClient.stop(); - } else if (cmd == '$') { - telnetClient.print("* Free RAM (bytes): "); - telnetClient.println(ESP.getFreeHeap()); - } else if (cmd == '!') { - resetESP(); - } else if (cmd == '&') { - _verboseMessages = !_verboseMessages; // toggle - telnetClient.printf("Suspend all messages is %s\n\r", _verboseMessages ? "disabled" : "enabled"); - } else { - // custom Project commands - if (_consoleCallbackProjectCmds) { - _consoleCallbackProjectCmds(); - } - } -} - -// Logger -// LOG_CONSOLE sends to the Telnet session -// LOG_HA sends to Telnet session plus a MQTT for Home Assistant -// LOG_NONE turns off all logging -void ESPHelper::logger(log_level_t level, const char * message) { - // do we log to the telnet window? - if ((level == LOG_CONSOLE) && (telnetClient && telnetClient.connected())) { - telnetClient.println(message); - telnetClient.flush(); - } else if (level == LOG_HA) { - char s[100]; - sprintf(s, "%s: %s\n", _hostname, message); // add new line, for the debug telnet printer - publish(MQTT_NOTIFICATION, s, false); - } - -// print to Serial if set in platform.io (requires recompile) -#ifdef USE_SERIAL - Serial.println(message); -#endif - -#ifdef USE_SERIAL1 - Serial.println(message); -#endif -} - -// send specific command to HA via MQTT -// format is: home//command/ -void ESPHelper::sendHACommand(const char * cmd) { - //logger(LOG_CONSOLE, "Sending command to HA..."); - - char s[100]; - sprintf(s, "%s%s/%s", MQTT_BASE, _hostname, MQTT_TOPIC_COMMAND); - - publish(s, cmd, false); -} - -// send specific start command to HA via MQTT, which returns the boottime -// format is: home//start -void ESPHelper::sendStart() { - //logger(LOG_CONSOLE, "Sending Start command to HA..."); - - char s[100]; - sprintf(s, "%s%s/%s", MQTT_BASE, _hostname, MQTT_TOPIC_START); - - // send initial payload of "start" to kick things off - publish(s, MQTT_TOPIC_START, false); -} diff --git a/src/ESPHelper.h b/src/ESPHelper.h deleted file mode 100644 index b50e6653e..000000000 --- a/src/ESPHelper.h +++ /dev/null @@ -1,219 +0,0 @@ -/* - ESPHelper.h - Copyright (c) 2017 ItKindaWorks Inc All right reserved. - github.com/ItKindaWorks - - This file is part of ESPHelper - - ESPHelper is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ESPHelper is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with ESPHelper. If not, see . -*/ -#pragma once - -#include -#include //https://github.com/esp8266/Arduino -#include -#include -#include -#include -#include -#include - -// MQTT stuff -#define DEFAULT_QOS 1 //at least once - devices are guarantee to get a message. -#define MQTT_BASE "home/" -#define MQTT_NOTIFICATION MQTT_BASE "notification" -#define MQTT_TOPIC_COMMAND "command" -#define MQTT_TOPIC_START "start" -#define MQTT_HA MQTT_BASE "ha" - -#define MAX_SUBSCRIPTIONS 25 // max # of subscriptions -#define MAX_TIME_INACTIVE 600000 // Max time for inactivity (ms) - 10 mins -#define TELNET_PORT 23 // telnet port -#define BUFFER_PRINT 500 // length of telnet buffer (default was 150) -#define COMMAND_LENGTH 20 // length of a command - -// ANSI Colors -#define COLOR_RESET "\x1B[0m" -#define COLOR_BLACK "\x1B[0;30m" -#define COLOR_RED "\x1B[0;31m" -#define COLOR_GREEN "\x1B[0;32m" -#define COLOR_YELLOW "\x1B[0;33m" -#define COLOR_BLUE "\x1B[0;34m" -#define COLOR_MAGENTA "\x1B[0;35m" -#define COLOR_CYAN "\x1B[0;36m" -#define COLOR_WHITE "\x1B[0;37m" - -// Logger -typedef enum { LOG_NONE, LOG_CONSOLE, LOG_HA } log_level_t; - -enum connStatus { NO_CONNECTION, BROADCAST, WIFI_ONLY, FULL_CONNECTION }; - -typedef struct { - const char * mqttHost; - const char * mqttUser; - const char * mqttPass; - uint16_t mqttPort; - const char * ssid; - const char * pass; -} netInfo; - -typedef struct { - bool isUsed = false; - const char * topic; -} subscription; - -typedef struct { - char key[10]; - char description[400]; -} command_t; - -// class ESPHelper { -class ESPHelper : public Print { - public: - void consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)()); - char * consoleGetLastCommand(); - void resetESP(); - void logger(log_level_t level, const char * message); - - virtual size_t write(uint8_t); - - ESPHelper(netInfo * startingNet); - - bool begin(const char * hostname, const char * app_name, const char * app_version); - - void end(); - - void useSecureClient(const char * fingerprint); - - int loop(); - - bool subscribe(const char * topic, uint8_t qos); - bool addSubscription(const char * topic); - bool removeSubscription(const char * topic); - bool unsubscribe(const char * topic); - bool addHASubscription(const char * topic); - - void publish(const char * topic, const char * payload); - void publish(const char * topic, const char * payload, bool retain); - - bool setCallback(MQTT_CALLBACK_SIGNATURE); - void setMQTTCallback(MQTT_CALLBACK_SIGNATURE); - - void setWifiCallback(void (*callback)()); - void setInitCallback(void (*callback)()); - - void sendHACommand(const char * s); - void sendStart(); - - void reconnect(); - - void updateNetwork(); - - const char * getSSID(); - void setSSID(const char * ssid); - - const char * getPASS(); - void setPASS(const char * pass); - - const char * getMQTTIP(); - void setMQTTIP(const char * mqttIP); - void setMQTTIP(const char * mqttIP, const char * mqttUser, const char * mqttPass); - - uint8_t getMQTTQOS(); - void setMQTTQOS(uint8_t qos); - - String getIP(); - IPAddress getIPAddress(); - - uint8_t getStatus(); - - void setNetInfo(netInfo newNetwork); - void setNetInfo(netInfo * newNetwork); - netInfo * getNetInfo(); - - void setHopping(bool canHop); - - void listSubscriptions(); - - void OTA_enable(); - void OTA_disable(); - void OTA_begin(); - - void setBoottime(const char * boottime); - - void consoleHandle(); - - private: - netInfo _currentNet; - PubSubClient client; - WiFiClient wifiClient; - WiFiClientSecure wifiClientSecure; - const char * _fingerprint; - bool _useSecureClient = false; - char _clientName[40]; - void (*_wifiCallback)(); - bool _wifiCallbackSet = false; - void (*_initCallback)(); - bool _initCallbackSet = false; - - std::function _mqttCallback; - - bool _mqttCallbackSet = false; - uint8_t _connectionStatus = NO_CONNECTION; - uint8_t _netCount = 0; - uint8_t _currentIndex = 0; - bool _ssidSet = false; - bool _passSet = false; - bool _mqttSet = false; - bool _mqttUserSet = false; - bool _mqttPassSet = false; - bool _useOTA = false; - bool _OTArunning = false; - bool _hoppingAllowed = false; - bool _hasBegun = false; - netInfo ** _netList; - bool _verboseMessages = true; - subscription _subscriptions[MAX_SUBSCRIPTIONS]; - char _hostname[24]; - uint8_t _qos = DEFAULT_QOS; - IPAddress _apIP = IPAddress(192, 168, 1, 254); - void changeNetwork(); - String macToStr(const uint8_t * mac); - bool checkParams(); - void resubscribe(); - uint8_t setConnectionStatus(); - - char _boottime[24]; - char _app_name[24]; - char _app_version[10]; - - // console/telnet specific - WiFiClient telnetClient; - - bool _telnetConnected = false; // Client is connected ? - bool _newLine = true; // New line write ? - - char _command[COMMAND_LENGTH]; // Command received, includes options seperated by a space - uint32_t _lastTimeCommand = millis(); // Last time command received - - command_t * _helpProjectCmds; // Help of commands setted by project - uint8_t _helpProjectCmds_count; // # available commands - - void (*_consoleCallbackProjectCmds)(); // Callable for projects commands - void consoleShowHelp(); - void consoleProcessCommand(); - bool isCRLF(char character); - - char bufferPrint[BUFFER_PRINT]; -}; diff --git a/src/boiler.ino b/src/boiler.ino index e236ad58e..2ab11596c 100644 --- a/src/boiler.ino +++ b/src/boiler.ino @@ -7,12 +7,14 @@ */ // local libraries -#include "ESPHelper.h" #include "ems.h" #include "emsuart.h" #include "my_config.h" #include "version.h" +// my libraries +#include + // public libraries #include // https://github.com/bblanchon/ArduinoJson #include // https://github.com/bakercp/CRC32 @@ -20,6 +22,8 @@ // standard arduino libs #include // https://github.com/esp8266/Arduino/tree/master/libraries/Ticker +#define myDebug(...) myESP.myDebug(__VA_ARGS__) + // timers, all values are in seconds #define PUBLISHVALUES_TIME 120 // every 2 minutes post HA values Ticker publishValuesTimer; @@ -27,11 +31,11 @@ Ticker publishValuesTimer; #define SYSTEMCHECK_TIME 10 // every 10 seconds check if Boiler is online and execute other requests Ticker systemCheckTimer; -#define REGULARUPDATES_TIME 60 // every minute a call is made +#define REGULARUPDATES_TIME 60 // every minute a call is made to fetch data from EMS devices manually Ticker regularUpdatesTimer; -#define HEARTBEAT_TIME 1 // every second blink heartbeat LED -Ticker heartbeatTimer; +#define LEDCHECK_TIME 1 // every second blink heartbeat LED +Ticker ledcheckTimer; // thermostat scan - for debugging Ticker scanThermostat; @@ -40,35 +44,25 @@ uint8_t scanThermostat_count; Ticker showerColdShotStopTimer; -// GPIOs -#define LED_HEARTBEAT LED_BUILTIN // onboard LED - -// hostname is also used as the MQTT topic identifier (home/) -#define HOSTNAME "boiler" - -// app specific - do not change -#define MQTT_BOILER MQTT_BASE HOSTNAME "/" -#define TOPIC_START MQTT_BOILER MQTT_TOPIC_START - // thermostat -#define TOPIC_THERMOSTAT_DATA MQTT_BOILER "thermostat_data" // for sending thermostat values -#define TOPIC_THERMOSTAT_CMD_TEMP MQTT_BOILER "thermostat_cmd_temp" // for received thermostat temp changes -#define TOPIC_THERMOSTAT_CMD_MODE MQTT_BOILER "thermostat_cmd_mode" // for received thermostat mode changes -#define TOPIC_THERMOSTAT_CURRTEMP "thermostat_currtemp" // current temperature -#define TOPIC_THERMOSTAT_SELTEMP "thermostat_seltemp" // selected temperature -#define TOPIC_THERMOSTAT_MODE "thermostat_mode" // mode +#define TOPIC_THERMOSTAT_DATA "thermostat_data" // for sending thermostat values +#define TOPIC_THERMOSTAT_CMD_TEMP "thermostat_cmd_temp" // for received thermostat temp changes +#define TOPIC_THERMOSTAT_CMD_MODE "thermostat_cmd_mode" // for received thermostat mode changes +#define THERMOSTAT_CURRTEMP "thermostat_currtemp" // current temperature +#define THERMOSTAT_SELTEMP "thermostat_seltemp" // selected temperature +#define THERMOSTAT_MODE "thermostat_mode" // mode // boiler -#define TOPIC_BOILER_DATA MQTT_BOILER "boiler_data" // for sending boiler values -#define TOPIC_BOILER_TAPWATER_ACTIVE MQTT_BOILER "tapwater_active" // if hot tap water is running -#define TOPIC_BOILER_HEATING_ACTIVE MQTT_BOILER "heating_active" // if heating is on +#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values +#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running +#define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on // shower time -#define TOPIC_SHOWERTIME MQTT_BOILER "showertime" // for sending shower time results -#define TOPIC_SHOWER_ALARM "shower_alarm" // for notifying HA that shower time has reached its limit -#define TOPIC_SHOWER_TIMER MQTT_BOILER "shower_timer" // toggle switch for enabling the shower logic -#define TOPIC_SHOWER_ALERT MQTT_BOILER "shower_alert" // toggle switch for enabling the shower alarm logic -#define TOPIC_SHOWER_COLDSHOT MQTT_BOILER "shower_coldshot" // used to trigger a coldshot from HA publish +#define TOPIC_SHOWERTIME "showertime" // for sending shower time results +#define TOPIC_SHOWER_TIMER "shower_timer" // toggle switch for enabling the shower logic +#define TOPIC_SHOWER_ALERT "shower_alert" // toggle switch for enabling the shower alarm logic +#define TOPIC_SHOWER_COLDSHOT "shower_coldshot" // used to trigger a coldshot from HA publish +#define SHOWER_ALARM "shower_alarm" // for notifying HA that shower time has reached its limit // logging - EMS_SYS_LOGGING_VERBOSE, EMS_SYS_LOGGING_NONE, EMS_SYS_LOGGING_BASIC (see ems.h) #define BOILER_DEFAULT_LOGGING EMS_SYS_LOGGING_NONE @@ -80,17 +74,16 @@ Ticker showerColdShotStopTimer; #undef SHOWER_MAX_DURATION #undef SHOWER_COLDSHOT_DURATION #undef SHOWER_OFFSET_TIME -const unsigned long SHOWER_PAUSE_TIME = 15000; // 15 seconds, max time if water is switched off & on during a shower -const unsigned long SHOWER_MIN_DURATION = 20000; // 20 secs, before recognizing its a shower -const unsigned long SHOWER_MAX_DURATION = 25000; // 25 secs, before trigger a shot of cold water -const unsigned long SHOWER_COLDSHOT_DURATION = 5; // in seconds! how long for cold water shot -const unsigned long SHOWER_OFFSET_TIME = 0; // 0 seconds grace time, to calibrate actual time under the shower +const unsigned long SHOWER_PAUSE_TIME = 15000; // 15 seconds, max time if water is switched off & on during a shower +const unsigned long SHOWER_MIN_DURATION = 20000; // 20 secs, before recognizing its a shower +const unsigned long SHOWER_MAX_DURATION = 25000; // 25 secs, before trigger a shot of cold water +const unsigned long SHOWER_COLDSHOT_DURATION = 5; // in seconds! how long for cold water shot +const unsigned long SHOWER_OFFSET_TIME = 0; // 0 seconds grace time, to calibrate actual time under the shower #endif typedef struct { - bool wifi_connected; bool shower_timer; // true if we want to report back on shower times - bool shower_alert; // true if we want the cold water reminder + bool shower_alert; // true if we want the alert of cold water } _Boiler_Status; typedef struct { @@ -101,19 +94,11 @@ typedef struct { bool doingColdShot; // true if we've just sent a jolt of cold water } _Boiler_Shower; -// ESPHelper -netInfo homeNet = {.mqttHost = MQTT_IP, - .mqttUser = MQTT_USER, - .mqttPass = MQTT_PASS, - .mqttPort = 1883, // this is the default, change if using another port - .ssid = WIFI_SSID, - .pass = WIFI_PASSWORD}; -ESPHelper myESP(&homeNet); - command_t PROGMEM project_cmds[] = { {"l [n]", "set logging (0=none, 1=raw, 2=basic, 3=thermostat only, 4=verbose)"}, {"s", "show statistics"}, + {"D", "auto Detect EMS connected devices"}, {"h", "list supported EMS telegram type IDs"}, {"M", "publish to MQTT"}, {"Q", "print Tx Queue"}, @@ -127,24 +112,14 @@ command_t PROGMEM project_cmds[] = { {"w [nn]", "set boiler warm water temperature (min 30)"}, {"a [n]", "set boiler warm tap water (0=off, 1=on)"}, {"T [xx]", "set thermostat temperature"}, - {"m [n]", "set thermostat mode (1=manual, 2=auto)"} - //{"U [c]", "do a thermostat scan on all ids (c=start id) for debugging only"} + {"m [n]", "set thermostat mode (0=low/night, 1=manual/day, 2=auto)"} }; -// calculates size of an 2d array at compile time -template -constexpr size_t ArraySize(T (&)[N]) { - return N; -} - // store for overall system status _Boiler_Status Boiler_Status; _Boiler_Shower Boiler_Shower; -// Debugger to telnet -#define myDebug(x, ...) myESP.printf(x, ##__VA_ARGS__); - // CRC checks uint32_t previousBoilerPublishCRC = 0; uint32_t previousThermostatPublishCRC = 0; @@ -154,19 +129,13 @@ const unsigned long POLL_TIMEOUT_ERR = 10000; // if no signal from boiler for la const unsigned long TX_HOLD_LED_TIME = 2000; // how long to hold the Tx LED because its so quick unsigned long timestamp; // for internal timings, via millis() -static int connectionStatus = NO_CONNECTION; -int boilerStatus = false; -bool startMQTTsent = false; uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off -// toggle for heartbeat LED -bool heartbeatEnabled; - // logging messages with fixed strings (newline done automatically) void myDebugLog(const char * s) { if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { - myDebug("%s\n", s); + myDebug(s); } } @@ -177,7 +146,7 @@ char * _float_to_char(char * a, float f, uint8_t precision = 1) { char * ret = a; // check for 0x8000 (sensor missing) if (f == EMS_VALUE_FLOAT_NOTSET) { - strcpy(ret, "?"); + strlcpy(ret, "?", sizeof(ret)); } else { long whole = (long)f; itoa(whole, a, 10); @@ -193,19 +162,19 @@ char * _float_to_char(char * a, float f, uint8_t precision = 1) { // convert bool to text char * _bool_to_char(char * s, uint8_t value) { if (value == EMS_VALUE_INT_ON) { - strcpy(s, "on"); + strlcpy(s, "on", sizeof(s)); } else if (value == EMS_VALUE_INT_OFF) { - strcpy(s, "off"); + strlcpy(s, "off", sizeof(s)); } else { - strcpy(s, "?"); + strlcpy(s, "?", sizeof(s)); } return s; } -// convert int to text value +// convert int (single byte) to text value char * _int_to_char(char * s, uint8_t value) { if (value == EMS_VALUE_INT_NOTSET) { - strcpy(s, "?"); + strlcpy(s, "?", sizeof(s)); } else { itoa(value, s, 10); } @@ -214,88 +183,150 @@ char * _int_to_char(char * s, uint8_t value) { // takes a float value at prints it to debug log void _renderFloatValue(const char * prefix, const char * postfix, float value) { - myDebug(" %s: ", prefix); - char s[20]; - myDebug("%s", _float_to_char(s, value)); + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + strlcat(buffer, _float_to_char(s, value), sizeof(buffer)); if (postfix != NULL) { - myDebug(" %s", postfix); + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); } - - myDebug("\n"); + myDebug(buffer); } -// takes an int value at prints it to debug log +// takes an int (single byte) value at prints it to debug log void _renderIntValue(const char * prefix, const char * postfix, uint8_t value) { - myDebug(" %s: ", prefix); - char s[20]; - myDebug("%s", _int_to_char(s, value)); + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + strlcat(buffer, _int_to_char(s, value), sizeof(buffer)); if (postfix != NULL) { - myDebug(" %s", postfix); + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); + } + myDebug(buffer); +} + +// takes an int value, converts to a fraction +void _renderIntfractionalValue(const char * prefix, const char * postfix, uint8_t value, uint8_t decimals) { + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + strlcat(buffer, _int_to_char(s, value / (decimals * 10)), sizeof(buffer)); + strlcat(buffer, ".", sizeof(buffer)); + strlcat(buffer, _int_to_char(s, value % (decimals * 10)), sizeof(buffer)); + + if (postfix != NULL) { + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); + } + myDebug(buffer); +} + +// takes a long value at prints it to debug log +void _renderLongValue(const char * prefix, const char * postfix, uint32_t value) { + char buffer[200] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + + if (value == EMS_VALUE_LONG_NOTSET) { + strlcat(buffer, "?", sizeof(buffer)); + } else { + char s[20] = {0}; + strlcat(buffer, ltoa(value, s, 10), sizeof(buffer)); } - myDebug("\n"); + if (postfix != NULL) { + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); + } + + myDebug(buffer); } // takes a bool value at prints it to debug log void _renderBoolValue(const char * prefix, uint8_t value) { - myDebug(" %s: ", prefix); - char s[20]; - myDebug("%s\n", _bool_to_char(s, value)); + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + + strlcat(buffer, _bool_to_char(s, value), sizeof(buffer)); + + myDebug(buffer); } // Show command - display stats on an 's' command void showInfo() { // General stats from EMS bus - myDebug("%sEMS-ESP-Boiler system stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" System logging is set to "); + myDebug("%sEMS-ESP-Boiler system stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); _EMS_SYS_LOGGING sysLog = ems_getLogging(); if (sysLog == EMS_SYS_LOGGING_BASIC) { - myDebug("Basic"); + myDebug(" System logging is set to Basic"); } else if (sysLog == EMS_SYS_LOGGING_VERBOSE) { - myDebug("Verbose"); + myDebug(" System logging is set to Verbose"); } else if (sysLog == EMS_SYS_LOGGING_THERMOSTAT) { - myDebug("Thermostat only"); + myDebug(" System logging is set to Thermostat only"); } else { - myDebug("None"); + myDebug(" System logging is set to None"); } - myDebug("\n # EMS type handlers: %d\n", ems_getEmsTypesCount()); + myDebug(" # EMS type handlers: %d", ems_getEmsTypesCount()); - myDebug(" Thermostat is %s, Poll is %s, Tx is %s, Shower Timer is %s, Shower Alert is %s\n", + myDebug(" Thermostat is %s, Boiler is %s, Poll is %s, Tx is %s, Shower Timer is %s, Shower Alert is %s", (ems_getThermostatEnabled() ? "enabled" : "disabled"), + (ems_getBoilerEnabled() ? "enabled" : "disabled"), ((EMS_Sys_Status.emsPollEnabled) ? "enabled" : "disabled"), ((EMS_Sys_Status.emsTxEnabled) ? "enabled" : "disabled"), ((Boiler_Status.shower_timer) ? "enabled" : "disabled"), ((Boiler_Status.shower_alert) ? "enabled" : "disabled")); - myDebug(" EMS Bus Stats: Connected=%s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d\n", - (ems_getBoilerEnabled() ? "yes" : "no"), + myDebug(" EMS Bus Stats: Connected=%s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d", + (ems_getBusConnected() ? "yes" : "no"), EMS_Sys_Status.emsRxPgks, EMS_Sys_Status.emsTxPkgs, EMS_Sys_Status.emxCrcErr); - myDebug("\n%sBoiler stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug(""); // newline? + + myDebug("%sBoiler stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + + // version details + char buffer_type[64]; + myDebug(" Boiler type: %s", ems_getBoilerType(buffer_type)); // active stats - myDebug(" Hot tap water is %s\n", (EMS_Boiler.tapwaterActive ? "running" : "off")); - myDebug(" Central Heating is %s\n", (EMS_Boiler.heatingActive ? "active" : "off")); + myDebug(" Hot tap water is %s", (EMS_Boiler.tapwaterActive ? "running" : "off")); + myDebug(" Central Heating is %s", (EMS_Boiler.heatingActive ? "active" : "off")); // UBAParameterWW _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); + myDebug(" Warm Water is set to %s", (EMS_Boiler.wWComfort ? "Comfort" : "ECO")); _renderIntValue("Warm Water selected temperature", "C", EMS_Boiler.wWSelTemp); _renderIntValue("Warm Water desired temperature", "C", EMS_Boiler.wWDesiredTemp); // UBAMonitorWWMessage _renderFloatValue("Warm Water current temperature", "C", EMS_Boiler.wWCurTmp); - _renderIntValue("Warm Water # starts", "times", EMS_Boiler.wWStarts); - myDebug(" Warm Water active time: %d days %d hours %d minutes\n", - EMS_Boiler.wWWorkM / 1440, - (EMS_Boiler.wWWorkM % 1440) / 60, - EMS_Boiler.wWWorkM % 60); + _renderIntfractionalValue("Warm Water current tapwater flow", "l/min", EMS_Boiler.wWCurFlow, 1); + _renderLongValue("Warm Water # starts", "times", EMS_Boiler.wWStarts); + if (EMS_Boiler.wWWorkM != EMS_VALUE_LONG_NOTSET) { + myDebug(" Warm Water active time: %d days %d hours %d minutes", + EMS_Boiler.wWWorkM / 1440, + (EMS_Boiler.wWWorkM % 1440) / 60, + EMS_Boiler.wWWorkM % 60); + } _renderBoolValue("Warm Water 3-way valve", EMS_Boiler.wWHeat); // UBAMonitorFast @@ -311,77 +342,90 @@ void showInfo() { _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); + myDebug(" Current System Service Code: %c%c", EMS_Boiler.serviceCodeChar1, EMS_Boiler.serviceCodeChar2); // UBAMonitorSlow _renderFloatValue("Outside temperature", "C", EMS_Boiler.extTemp); _renderFloatValue("Boiler temperature", "C", EMS_Boiler.boilTemp); _renderIntValue("Pump modulation", "%", EMS_Boiler.pumpMod); - _renderIntValue("Burner # restarts", "times", EMS_Boiler.burnStarts); - myDebug(" Total burner operating time: %d days %d hours %d minutes\n", - EMS_Boiler.burnWorkMin / 1440, - (EMS_Boiler.burnWorkMin % 1440) / 60, - EMS_Boiler.burnWorkMin % 60); - myDebug(" Total heat operating time: %d days %d hours %d minutes\n", - EMS_Boiler.heatWorkMin / 1440, - (EMS_Boiler.heatWorkMin % 1440) / 60, - EMS_Boiler.heatWorkMin % 60); + _renderLongValue("Burner # restarts", "times", EMS_Boiler.burnStarts); + if (EMS_Boiler.burnWorkMin != EMS_VALUE_LONG_NOTSET) { + myDebug(" Total burner operating time: %d days %d hours %d minutes", + EMS_Boiler.burnWorkMin / 1440, + (EMS_Boiler.burnWorkMin % 1440) / 60, + EMS_Boiler.burnWorkMin % 60); + } + if (EMS_Boiler.heatWorkMin != EMS_VALUE_LONG_NOTSET) { + myDebug(" 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(" Total UBA working time: %d days %d hours %d minutes", + EMS_Boiler.UBAuptime / 1440, + (EMS_Boiler.UBAuptime % 1440) / 60, + EMS_Boiler.UBAuptime % 60); + } + + myDebug(""); // newline // Thermostat stats if (ems_getThermostatEnabled()) { - myDebug("\n%sThermostat stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" Thermostat type: "); - ems_printThermostatType(); - myDebug("\n Thermostat time is "); - if (EMS_ID_THERMOSTAT != EMS_ID_THERMOSTAT_EASY) { - myDebug("%02d:%02d:%02d %d/%d/%d\n", + myDebug("%sThermostat stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug(" Thermostat type: %s", ems_getThermostatType(buffer_type)); + if (ems_getThermostatModel() != EMS_MODEL_EASY) { + myDebug(" Thermostat time is %02d:%02d:%02d %d/%d/%d", EMS_Thermostat.hour, EMS_Thermostat.minute, EMS_Thermostat.second, EMS_Thermostat.day, EMS_Thermostat.month, EMS_Thermostat.year + 2000); - } else { - myDebug("\n"); } _renderFloatValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp); _renderFloatValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp); - myDebug(" Mode is set to "); if (EMS_Thermostat.mode == 0) { - myDebug("low\n"); + myDebug(" Mode is set to low"); } else if (EMS_Thermostat.mode == 1) { - myDebug("manual\n"); + myDebug(" Mode is set to manual"); } else if (EMS_Thermostat.mode == 2) { - myDebug("auto\n"); + myDebug(" Mode is set to auto"); } else { - myDebug("?\n"); - // myDebug("? (value is %d)\n", EMS_Thermostat.mode); + myDebug(" Mode is set to ?"); } } + myDebug(""); // newline + // show the Shower Info if (Boiler_Status.shower_timer) { - myDebug("\n%s Shower stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" Shower Timer is %s\n", (Boiler_Shower.showerOn ? "active" : "off")); + myDebug("%s Shower stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug(" Shower Timer is %s", (Boiler_Shower.showerOn ? "active" : "off")); } - - myDebug("\n"); } // send values to HA via MQTT // a json object is created for the boiler and one for the thermostat // CRC check is done to see if there are changes in the values since the last send to avoid too much wifi traffic void publishValues(bool force) { - char s[20]; // for formatting strings + char s[20] = {0}; // for formatting strings // Boiler values as one JSON object StaticJsonBuffer<512> jsonBuffer; char data[512]; JsonObject & rootBoiler = jsonBuffer.createObject(); + size_t rlen; + CRC32 crc; + uint32_t fchecksum; rootBoiler["wWSelTemp"] = _int_to_char(s, EMS_Boiler.wWSelTemp); rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); + rootBoiler["wWComfort"] = EMS_Boiler.wWComfort ? "Comfort" : "ECO"; rootBoiler["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); + snprintf(s, sizeof(s), "%i.%i", EMS_Boiler.wWCurFlow / 10, EMS_Boiler.wWCurFlow % 10); + rootBoiler["wWCurFlow"] = s; rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); rootBoiler["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp); rootBoiler["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp); @@ -395,34 +439,31 @@ void publishValues(bool force) { rootBoiler["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); + snprintf(s, sizeof(s), "%c%c", EMS_Boiler.serviceCodeChar1, EMS_Boiler.serviceCodeChar2); + rootBoiler["ServiceCode"] = s; - size_t len = rootBoiler.measureLength(); - rootBoiler.printTo(data, len + 1); // form the json string + rlen = rootBoiler.measureLength(); + rootBoiler.printTo(data, rlen + 1); // form the json string // calculate hash and send values if something has changed, to save unnecessary wifi traffic - CRC32 crc; - for (size_t i = 0; i < len - 1; i++) { + for (size_t i = 0; i < rlen - 1; i++) { crc.update(data[i]); } - uint32_t checksum = crc.finalize(); - if ((previousBoilerPublishCRC != checksum) || force) { - previousBoilerPublishCRC = checksum; - if (ems_getLogging() >= EMS_SYS_LOGGING_BASIC) { - myDebug("Publishing boiler data via MQTT\n"); - } + fchecksum = crc.finalize(); + if ((previousBoilerPublishCRC != fchecksum) || force) { + previousBoilerPublishCRC = fchecksum; + myDebugLog("Publishing boiler data via MQTT"); // send values via MQTT - myESP.publish(TOPIC_BOILER_DATA, data); + 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) { - if (ems_getLogging() >= EMS_SYS_LOGGING_BASIC) { - myDebug("Publishing hot water and heating state via MQTT\n"); - } - myESP.publish(TOPIC_BOILER_TAPWATER_ACTIVE, EMS_Boiler.tapwaterActive == 1 ? "1" : "0"); - myESP.publish(TOPIC_BOILER_HEATING_ACTIVE, EMS_Boiler.heatingActive == 1 ? "1" : "0"); + myDebugLog("Publishing hot water and heating state 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 } @@ -434,36 +475,34 @@ void publishValues(bool force) { return; // build json object - JsonObject & rootThermostat = jsonBuffer.createObject(); - rootThermostat[TOPIC_THERMOSTAT_CURRTEMP] = _float_to_char(s, EMS_Thermostat.curr_roomTemp); - rootThermostat[TOPIC_THERMOSTAT_SELTEMP] = _float_to_char(s, EMS_Thermostat.setpoint_roomTemp); + JsonObject & rootThermostat = jsonBuffer.createObject(); + rootThermostat[THERMOSTAT_CURRTEMP] = _float_to_char(s, EMS_Thermostat.curr_roomTemp); + rootThermostat[THERMOSTAT_SELTEMP] = _float_to_char(s, EMS_Thermostat.setpoint_roomTemp); // send mode 0=low, 1=manual, 2=auto if (EMS_Thermostat.mode == 0) { - rootThermostat[TOPIC_THERMOSTAT_MODE] = "low"; + rootThermostat[THERMOSTAT_MODE] = "low"; } else if (EMS_Thermostat.mode == 1) { - rootThermostat[TOPIC_THERMOSTAT_MODE] = "manual"; + rootThermostat[THERMOSTAT_MODE] = "manual"; } else { - rootThermostat[TOPIC_THERMOSTAT_MODE] = "auto"; + rootThermostat[THERMOSTAT_MODE] = "auto"; } - size_t len = rootThermostat.measureLength(); - rootThermostat.printTo(data, len + 1); // form the json string + rlen = rootThermostat.measureLength(); + rootThermostat.printTo(data, rlen + 1); // form the json string // calculate new CRC crc.reset(); - for (size_t i = 0; i < len - 1; i++) { + for (size_t i = 0; i < rlen - 1; i++) { crc.update(data[i]); } uint32_t checksum = crc.finalize(); if ((previousThermostatPublishCRC != checksum) || force) { previousThermostatPublishCRC = checksum; - if (ems_getLogging() >= EMS_SYS_LOGGING_BASIC) { - myDebug("Publishing thermostat data via MQTT\n"); - } + myDebugLog("Publishing thermostat data via MQTT"); // send values via MQTT - myESP.publish(TOPIC_THERMOSTAT_DATA, data); + myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); } } } @@ -471,14 +510,14 @@ void publishValues(bool force) { // sets the shower timer on/off void set_showerTimer() { if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { - myDebug("Shower timer is %s\n", Boiler_Status.shower_timer ? "enabled" : "disabled"); + myDebug("Shower timer is %s", Boiler_Status.shower_timer ? "enabled" : "disabled"); } } // sets the shower alert on/off void set_showerAlert() { if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { - myDebug("Shower alert is %s\n", Boiler_Status.shower_alert ? "enabled" : "disabled"); + myDebug("Shower alert is %s", Boiler_Status.shower_alert ? "enabled" : "disabled"); } } @@ -503,7 +542,6 @@ void myDebugCallback() { ems_setTxEnabled(b); break; case 'M': - //myESP.logger(LOG_HA, "Force publish values"); publishValues(true); break; case 'h': // show type handlers @@ -511,39 +549,38 @@ void myDebugCallback() { break; case 'S': // toggle Shower timer support Boiler_Status.shower_timer = !Boiler_Status.shower_timer; - myESP.publish(TOPIC_SHOWER_TIMER, Boiler_Status.shower_timer ? "1" : "0"); + myESP.mqttPublish(TOPIC_SHOWER_TIMER, Boiler_Status.shower_timer ? "1" : "0"); break; case 'A': // toggle Shower alert Boiler_Status.shower_alert = !Boiler_Status.shower_alert; - myESP.publish(TOPIC_SHOWER_ALERT, Boiler_Status.shower_alert ? "1" : "0"); + myESP.mqttPublish(TOPIC_SHOWER_ALERT, Boiler_Status.shower_alert ? "1" : "0"); break; - case 'Q': //print Tx Queue + case 'Q': // print Tx Queue ems_printTxQueue(); break; + case 'D': // Auto detect EMS devices + ems_getVersions(); + break; default: - myDebug("Unknown command. Use ? for help.\n"); + myDebug("Unknown command. Use ? for help."); break; } return; } - // for commands with parameters, assume command is just one letter + // for commands with parameters, assume command is just a single letter followed by a space switch (cmd[0]) { case 'T': // set thermostat temp ems_setThermostatTemp(strtof(&cmd[2], 0)); break; case 'm': // set thermostat mode - if ((cmd[2] - '0') == 1) - ems_setThermostatMode(1); - else if ((cmd[2] - '0') == 2) - ems_setThermostatMode(2); + ems_setThermostatMode(cmd[2] - '0'); break; case 'w': // set warm water temp ems_setWarmWaterTemp((uint8_t)strtol(&cmd[2], 0, 10)); break; case 'l': // logging ems_setLogging((_EMS_SYS_LOGGING)(cmd[2] - '0')); - updateHeartbeat(); break; case 'a': // set ww activate on or off if ((cmd[2] - '0') == 1) @@ -552,21 +589,21 @@ void myDebugCallback() { ems_setWarmTapWaterActivated(false); break; case 'b': // boiler read command - ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_ID_BOILER); + ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_Boiler.type_id); break; case 't': // thermostat command - ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_ID_THERMOSTAT); + ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_Thermostat.type_id); break; case 'r': // send raw data ems_sendRawTelegram(&cmd[2]); break; case 'x': // experimental, not displayed! - myDebug("Calling experimental...\n"); + myDebug("Calling experimental..."); ems_setLogging(EMS_SYS_LOGGING_VERBOSE); ems_setExperimental((uint8_t)strtol(&cmd[2], 0, 16)); // takes HEX param break; case 'U': // thermostat scan - myDebug("Doing a type ID scan on thermostat...\n"); + myDebug("Doing a type ID scan on thermostat..."); ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); publishValuesTimer.detach(); systemCheckTimer.detach(); @@ -575,111 +612,88 @@ void myDebugCallback() { scanThermostat.attach(SCANTHERMOSTAT_TIME, do_scanThermostat); break; default: - myDebug("Unknown command. Use ? for help.\n"); + myDebug("Unknown command. Use ? for help."); break; } return; } // MQTT Callback to handle incoming/outgoing changes -void MQTTcallback(char * topic, byte * payload, uint8_t length) { - // check if start is received, if so return boottime - defined in ESPHelper.h - if (strcmp(topic, TOPIC_START) == 0) { - payload[length] = '\0'; // add null terminator - //myDebug("MQTT topic boottime: %s\n", payload); - myESP.setBoottime((char *)payload); - return; +void MQTTcallback(unsigned int type, const char * topic, const char * message) { + // we're connected. lets subscribe to some topics + if (type == MQTT_CONNECT_EVENT) { + myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_TEMP); + myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE); + myESP.mqttSubscribe(TOPIC_SHOWER_TIMER); + myESP.mqttSubscribe(TOPIC_SHOWER_ALERT); + myESP.mqttSubscribe(TOPIC_BOILER_TAPWATER_ACTIVE); + myESP.mqttSubscribe(TOPIC_BOILER_HEATING_ACTIVE); + myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT); + + // publish to HA the status of the Shower parameters + myESP.mqttPublish(TOPIC_SHOWER_TIMER, Boiler_Status.shower_timer ? "1" : "0"); + myESP.mqttPublish(TOPIC_SHOWER_ALERT, Boiler_Status.shower_alert ? "1" : "0"); } - // thermostat temp changes - if (strcmp(topic, TOPIC_THERMOSTAT_CMD_TEMP) == 0) { - float f = strtof((char *)payload, 0); - char s[10]; - myDebug("MQTT topic: thermostat temp value %s\n", _float_to_char(s, f)); - ems_setThermostatTemp(f); - // publish back so HA is immediately updated - publishValues(true); - return; - } - - // thermostat mode changes - if (strcmp(topic, TOPIC_THERMOSTAT_CMD_MODE) == 0) { - payload[length] = '\0'; // add null terminator - myDebug("MQTT topic: thermostat mode value %s\n", payload); - if (strcmp((char *)payload, "auto") == 0) { - ems_setThermostatMode(2); - } else if (strcmp((char *)payload, "manual") == 0) { - ems_setThermostatMode(1); + if (type == MQTT_MESSAGE_EVENT) { + // thermostat temp changes + if (strcmp(topic, TOPIC_THERMOSTAT_CMD_TEMP) == 0) { + float f = strtof((char *)message, 0); + char s[10] = {0}; + myDebug("MQTT topic: thermostat temperature value %s", _float_to_char(s, f)); + ems_setThermostatTemp(f); + publishValues(true); // publish back so HA is immediately updated } - return; - } - // shower timer - if (strcmp(topic, TOPIC_SHOWER_TIMER) == 0) { - if (payload[0] == '1') { - Boiler_Status.shower_timer = true; - } else if (payload[0] == '0') { - Boiler_Status.shower_timer = false; + // thermostat mode changes + if (strcmp(topic, TOPIC_THERMOSTAT_CMD_MODE) == 0) { + myDebug("MQTT topic: thermostat mode value %s", message); + if (strcmp((char *)message, "auto") == 0) { + ems_setThermostatMode(2); + } else if (strcmp((char *)message, "manual") == 0) { + ems_setThermostatMode(1); + } } - set_showerTimer(); - return; - } - // shower alert - if (strcmp(topic, TOPIC_SHOWER_ALERT) == 0) { - if (payload[0] == '1') { - Boiler_Status.shower_alert = true; - } else if (payload[0] == '0') { - Boiler_Status.shower_alert = false; + // shower timer + if (strcmp(topic, TOPIC_SHOWER_TIMER) == 0) { + if (message[0] == '1') { + Boiler_Status.shower_timer = true; + } else if (message[0] == '0') { + Boiler_Status.shower_timer = false; + } + set_showerTimer(); } - set_showerAlert(); - return; - } - // shower cold shot - if (strcmp(topic, TOPIC_SHOWER_COLDSHOT) == 0) { - _showerColdShotStart(); - return; - } + // shower alert + if (strcmp(topic, TOPIC_SHOWER_ALERT) == 0) { + if (message[0] == '1') { + Boiler_Status.shower_alert = true; + } else if (message[0] == '0') { + Boiler_Status.shower_alert = false; + } + set_showerAlert(); + } - // if HA is booted, restart device too - if (strcmp(topic, MQTT_HA) == 0) { - payload[length] = '\0'; // add null terminator - if (strcmp((char *)payload, "start") == 0) { - myDebug("HA rebooted - restarting device\n"); - myESP.resetESP(); + // shower cold shot + if (strcmp(topic, TOPIC_SHOWER_COLDSHOT) == 0) { + _showerColdShotStart(); } } } -// Init callback, which is used to set functions and call methods when telnet has started -void InitCallback() { - ems_setLogging(BOILER_DEFAULT_LOGGING); // turn off logging as default startup -} - -// WifiCallback, called when a WiFi connect has successfully been established -void WIFIcallback() { - Boiler_Status.wifi_connected = true; - -#ifdef USE_LED - digitalWrite(LED_HEARTBEAT, HIGH); -#endif - - // when finally we're all set up, we can fire up the uart (this will enable the UART interrupts) +// Init callback, which is used to set functions and call methods after a wifi connection has been established +void WIFICallback() { +// when finally we're all set up, we can fire up the uart (this will enable the UART interrupts) +#ifdef DEBUG_SUPPORT + myDebug("Warning, in DEBUG mode. EMS bus is disabled. See -DDEBUG_SUPPORT build option."); +#else emsuart_init(); -} - -// Sets the LED heartbeat depending on the logging setting -void updateHeartbeat() { - _EMS_SYS_LOGGING logSetting = ems_getLogging(); - if (logSetting == EMS_SYS_LOGGING_VERBOSE) { - heartbeatEnabled = true; - } else { - heartbeatEnabled = false; -#ifdef USE_LED - digitalWrite(LED_HEARTBEAT, HIGH); // ...and turn off LED #endif - } + + // now that we're connected, send a version request to see what things are on the EMS bus + myDebug("Starting up. Finding what devices are on the EMS bus..."); + ems_getVersions(); } // Initialize the boiler settings @@ -687,10 +701,6 @@ void initBoiler() { // default settings Boiler_Status.shower_timer = BOILER_SHOWER_TIMER; Boiler_Status.shower_alert = BOILER_SHOWER_ALERT; - ems_setThermostatEnabled(BOILER_THERMOSTAT_ENABLED); - - // init boiler - Boiler_Status.wifi_connected = false; // init shower Boiler_Shower.timerStart = 0; @@ -698,9 +708,9 @@ void initBoiler() { Boiler_Shower.duration = 0; Boiler_Shower.doingColdShot = false; - // heartbeat only if verbose logging - ems_setLogging(BOILER_DEFAULT_LOGGING); - updateHeartbeat(); + ems_setLogging(BOILER_DEFAULT_LOGGING); // set default logging + + ems_init(); // call ems.cpp's init function to set all the internal params } // call PublishValues without forcing, so using CRC to see if we really need to publish @@ -708,95 +718,40 @@ void do_publishValues() { publishValues(false); } -// -// SETUP -// Note: we don't init the UART here as we should wait until everything is loaded first. It's done in loop() -// -void setup() { -#ifdef USE_LED - // set pin for LEDs - start up with all lit up while we sort stuff out - pinMode(LED_HEARTBEAT, OUTPUT); - digitalWrite(LED_HEARTBEAT, LOW); // onboard LED is on - heartbeatTimer.attach(HEARTBEAT_TIME, heartbeat); // blink heartbeat LED -#endif - - // Timers using Ticker library - publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post HA values - systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if Boiler is online - regularUpdatesTimer.attach(REGULARUPDATES_TIME, regularUpdates); // regular reads from the EMS - - // set up WiFi - myESP.setWifiCallback(WIFIcallback); - - // set up MQTT - myESP.setMQTTCallback(MQTTcallback); - myESP.addSubscription(MQTT_HA); - myESP.addSubscription(TOPIC_START); - myESP.addSubscription(TOPIC_THERMOSTAT_CMD_TEMP); - myESP.addSubscription(TOPIC_THERMOSTAT_CMD_MODE); - myESP.addSubscription(TOPIC_SHOWER_TIMER); - myESP.addSubscription(TOPIC_SHOWER_ALERT); - myESP.addSubscription(TOPIC_BOILER_TAPWATER_ACTIVE); - myESP.addSubscription(TOPIC_BOILER_HEATING_ACTIVE); - myESP.addSubscription(TOPIC_SHOWER_COLDSHOT); - - myESP.setInitCallback(InitCallback); - - myESP.consoleSetCallBackProjectCmds(project_cmds, ArraySize(project_cmds), myDebugCallback); // set up Telnet commands - myESP.begin(HOSTNAME, APP_NAME, APP_VERSION); // start wifi and mqtt services - - // init ems statisitcs - ems_init(); - - // init Boiler specific parameters - initBoiler(); -} - -// heartbeat callback to light up the LED, called via Ticker -void heartbeat() { - if (heartbeatEnabled) { -#ifdef USE_LED - int state = digitalRead(LED_HEARTBEAT); - digitalWrite(LED_HEARTBEAT, !state); -#endif +// callback to light up the LED, called via Ticker every second +void do_ledcheck() { +#ifndef NO_LED + int state; + if (ems_getBusConnected()) { + state = HIGH; + } else { + state = !digitalRead(BOILER_LED); } + WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << BOILER_LED)); // toggle LED. 4 is on. 8 is off +#endif } // Thermostat scan void do_scanThermostat() { - //myDebug("Scanning %d..\n", scanThermostat_count); - ems_doReadCommand(scanThermostat_count, EMS_ID_THERMOSTAT); + myDebug("Scanning thermostat type calls, starting at %d...", scanThermostat_count); + ems_doReadCommand(scanThermostat_count, EMS_Thermostat.type_id); scanThermostat_count++; } -// do a healthcheck every now and then to see if we connections +// do a system health check every now and then to see if we all connections void do_systemCheck() { - // first do a system check to see if there is still a connection to the EMS - if (!ems_getBoilerEnabled()) { - myDebug("Error! Unable to connect to EMS bus. Please check connections. Retry in %d seconds...\n", + if (!ems_getBusConnected()) { + myDebug("Error! Unable to connect to EMS bus. Please make sure you're not in DEBUG_SUPPORT mode. Retrying in %d seconds...", SYSTEMCHECK_TIME); } -} - -// EMS telegrams to send after startup -void firstTimeFetch() { - ems_doReadCommand(EMS_TYPE_UBAMonitorFast, EMS_ID_BOILER); // get boiler stats which usually comes every 10 sec - ems_doReadCommand(EMS_TYPE_UBAMonitorSlow, EMS_ID_BOILER); // get boiler stats which usually comes every 60 sec - ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_ID_BOILER); // get Warm Water values - - if (ems_getThermostatEnabled()) { - ems_getThermostatValues(); // get Thermostat temps (if supported) - ems_doReadCommand(EMS_TYPE_RCTime, EMS_ID_THERMOSTAT); // get Thermostat time - } + ems_setBusConnected(false); // for the bus to be offline, it'll come back on the next Poll } // force calls to get data from EMS for the types that aren't sent as broadcasts -void regularUpdates() { - ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_ID_BOILER); // get Warm Water values - - if (ems_getThermostatEnabled()) { - ems_getThermostatValues(); // get Thermostat temps (if supported) - } +void do_regularUpdates() { + myDebugLog("Calling schedule fetch of values from EMS devices.."); + ems_getThermostatValues(); // get Thermostat temps (if supported) + ems_getBoilerValues(); } // turn off hot water to send a shot of cold @@ -814,6 +769,7 @@ void _showerColdShotStop() { myDebugLog("Shower: finished shot of cold. hot water back on"); ems_setWarmTapWaterActivated(true); Boiler_Shower.doingColdShot = false; + // disable the timer showerColdShotStopTimer.detach(); } } @@ -834,26 +790,19 @@ void showerCheck() { Boiler_Shower.doingColdShot = false; Boiler_Shower.duration = 0; Boiler_Shower.showerOn = false; -#ifdef SHOWER_TEST myDebugLog("Shower: hot water on..."); -#endif } else { // hot water has been on for a while // first check to see if hot water has been on long enough to be recognized as a Shower/Bath if (!Boiler_Shower.showerOn && (timestamp - Boiler_Shower.timerStart) > SHOWER_MIN_DURATION) { Boiler_Shower.showerOn = true; -#ifdef SHOWER_TEST - myDebugLog("Shower: hot water still running, starting shower timer"); -#endif } // check if the shower has been on too long else if ((((timestamp - Boiler_Shower.timerStart) > SHOWER_MAX_DURATION) && !Boiler_Shower.doingColdShot) && Boiler_Status.shower_alert) { - myESP.sendHACommand(TOPIC_SHOWER_ALARM); -#ifdef SHOWER_TEST + myESP.sendHACommand(SHOWER_ALARM); myDebugLog("Shower: exceeded max shower time"); -#endif _showerColdShotStart(); } } @@ -861,43 +810,31 @@ void showerCheck() { // if it just turned off, record the time as it could be a short pause if ((Boiler_Shower.timerStart != 0) && (Boiler_Shower.timerPause == 0)) { Boiler_Shower.timerPause = timestamp; -#ifdef SHOWER_TEST myDebugLog("Shower: hot water turned off"); -#endif } // if shower has been off for longer than the wait time if ((Boiler_Shower.timerPause != 0) && ((timestamp - Boiler_Shower.timerPause) > SHOWER_PAUSE_TIME)) { - /* - sprintf(s, - "Shower: duration %d offset %d", - (Boiler_Shower.timerPause - Boiler_Shower.timerStart), - SHOWER_OFFSET_TIME); - myDebugLog("s"); - */ - // it is over the wait period, so assume that the shower has finished and calculate the total time and publish // because its unsigned long, can't have negative so check if length is less than OFFSET_TIME if ((Boiler_Shower.timerPause - Boiler_Shower.timerStart) > SHOWER_OFFSET_TIME) { Boiler_Shower.duration = (Boiler_Shower.timerPause - Boiler_Shower.timerStart - SHOWER_OFFSET_TIME); if (Boiler_Shower.duration > SHOWER_MIN_DURATION) { - char s[50]; - sprintf(s, - "%d minutes and %d seconds", - (uint8_t)((Boiler_Shower.duration / (1000 * 60)) % 60), - (uint8_t)((Boiler_Shower.duration / 1000) % 60)); - + char s[50] = {0}; + char buffer[16] = {0}; + strlcpy(s, itoa((uint8_t)((Boiler_Shower.duration / (1000 * 60)) % 60), buffer, 10), sizeof(s)); + strlcat(s, " minutes and ", sizeof(s)); + strlcat(s, itoa((uint8_t)((Boiler_Shower.duration / 1000) % 60), buffer, 10), sizeof(s)); + strlcat(s, " seconds", sizeof(s)); if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { - myDebug("Shower: finished with duration %s\n", s); + myDebug("Shower: finished with duration %s", s); } - myESP.publish(TOPIC_SHOWERTIME, s); // publish to HA + myESP.mqttPublish(TOPIC_SHOWERTIME, s); // publish to HA } } -#ifdef SHOWER_TEST // reset everything myDebugLog("Shower: resetting timers"); -#endif Boiler_Shower.timerStart = 0; Boiler_Shower.timerPause = 0; Boiler_Shower.showerOn = false; @@ -907,39 +844,40 @@ void showerCheck() { } } +// +// SETUP +// +void setup() { +#ifndef NO_LED + // set pin for LED + pinMode(BOILER_LED, OUTPUT); + digitalWrite(BOILER_LED, (BOILER_LED == LED_BUILTIN) ? HIGH : LOW); // light on. For onboard high=off + ledcheckTimer.attach(LEDCHECK_TIME, do_ledcheck); // blink heartbeat LED +#endif + + // Timers using Ticker library + publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post HA values + systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if Boiler is online + regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS + + // set up myESP for Wifi, MQTT, MDNS and Telnet + myESP.setup(APP_HOSTNAME, APP_NAME, APP_VERSION, WIFI_SSID, WIFI_PASSWORD, MQTT_IP, MQTT_USER, MQTT_PASS); + myESP.consoleSetCallBackProjectCmds(project_cmds, ArraySize(project_cmds), myDebugCallback); // set up Telnet commands + myESP.setWIFICallback(WIFICallback); + myESP.setMQTTCallback(MQTTcallback); + + // init Boiler specific parameters + initBoiler(); +} + // // Main loop // void loop() { - connectionStatus = myESP.loop(); - timestamp = millis(); + timestamp = millis(); - // update the Rx Tx and ERR LEDs -#ifdef USE_LED - showLEDs(); -#endif - - // do not continue unless we have a wifi connection - if (connectionStatus < WIFI_ONLY) { - return; - } - - // if this is the first time we've connected to MQTT, send a welcome start message - // which will send all the state values from HA back to the clock via MQTT and return the boottime - if ((!startMQTTsent) && (connectionStatus == FULL_CONNECTION)) { - myESP.sendStart(); - startMQTTsent = true; - - // publish to HA the status of the Shower parameters - myESP.publish(TOPIC_SHOWER_TIMER, Boiler_Status.shower_timer ? "1" : "0"); - myESP.publish(TOPIC_SHOWER_ALERT, Boiler_Status.shower_alert ? "1" : "0"); - } - - // if the EMS bus has just connected, send a request to fetch some initial values - if (ems_getBoilerEnabled() && boilerStatus == false) { - boilerStatus = true; - firstTimeFetch(); - } + // the main loop + myESP.loop(); // publish the values to MQTT, regardless if the values haven't changed if (ems_getEmsRefreshed()) { @@ -951,6 +889,4 @@ void loop() { if (Boiler_Status.shower_timer) { showerCheck(); } - - yield(); // yield to prevent watchdog from timing out } diff --git a/src/ems.cpp b/src/ems.cpp index 05cb30c2e..fe5bc5b4b 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -5,46 +5,24 @@ */ #include "ems.h" -#include "emsuart.h" -#include -#include // https://github.com/rlogiacco/CircularBuffer -#include - -// Check for ESPurna vs ESPHelper (standalone) -#ifdef USE_CUSTOM_H -#include "debug.h" -extern void debugSend(const char * format, ...); -#define myDebug(...) debugSend(__VA_ARGS__) -#else -#include -extern ESPHelper myESP; -#define myDebug(x, ...) myESP.printf(x, ##__VA_ARGS__) -#endif - -// include custom configuration settings -#include "my_config.h" - -// calculates size of an 2d array at compile time -template -constexpr size_t ArraySize(T (&)[N]) { - return N; -} +// myESP +#define myDebug(...) myESP.myDebug(__VA_ARGS__) _EMS_Sys_Status EMS_Sys_Status; // EMS Status -CircularBuffer<_EMS_TxTelegram, 20> EMS_TxQueue; // FIFO queue for Tx send buffer +CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO queue for Tx send buffer // callbacks per type +// Boiler and Buderus devices +void _process_Version(uint8_t * data, uint8_t length); void _process_UBAMonitorFast(uint8_t * data, uint8_t length); void _process_UBAMonitorSlow(uint8_t * data, uint8_t length); void _process_UBAMonitorWWMessage(uint8_t * data, uint8_t length); void _process_UBAParameterWW(uint8_t * data, uint8_t length); +void _process_UBATotalUptimeMessage(uint8_t * data, uint8_t length); -// Thermostat - -// Common -void _process_Version(uint8_t * data, uint8_t length); +// Common for most thermostats void _process_SetPoints(uint8_t * data, uint8_t length); void _process_RCTime(uint8_t * data, uint8_t length); void _process_RCOutdoorTempMessage(uint8_t * data, uint8_t length); @@ -57,49 +35,95 @@ void _process_RC20StatusMessage(uint8_t * data, uint8_t length); void _process_RC30Set(uint8_t * data, uint8_t length); void _process_RC30StatusMessage(uint8_t * data, uint8_t length); +// RC35 +void _process_RC35Set(uint8_t * data, uint8_t length); +void _process_RC35StatusMessage(uint8_t * data, uint8_t length); + +// RC35 + // Easy void _process_EasyStatusMessage(uint8_t * data, uint8_t length); -const _Thermostat_Types Thermostat_Types[] = { +// EMS types for known Buderus devices +// Note: This is still incomplete +const _Model_Type Model_Types[] = { - {EMS_ID_THERMOSTAT_RC20, "RC20 (Nefit Moduline 300)"}, - {EMS_ID_THERMOSTAT_RC30, "RC30 (Nefit Moduline 400)"}, - {EMS_ID_THERMOSTAT_EASY, "TC100 (Nefit Easy/CT100)"} + // me + {EMS_MODEL_SERVICEKEY, 999, 0x0B, "Service Key"}, + + // various boilers and buderus type devices + {EMS_MODEL_UBA, 123, 0x08, "MC10/UBA3 Boiler"}, // verified + {EMS_MODEL_BK15, 64, 0x08, "Sieger BK15 Boiler"}, // verified + {EMS_MODEL_BC10, 190, 0x09, "BC10 Base Controller"}, // verified + {EMS_MODEL_MM10, 125, 0x21, "MM10 Mixer Module"}, // warning, fake product id! + {EMS_MODEL_WM10, 126, 0x11, "WM10 Switch Module"}, // warning, fake product id! + + // controllers and thermostats + {EMS_MODEL_ES73, 76, 0x10, "Sieger ES73"}, + {EMS_MODEL_RC20, 77, 0x17, "RC20 (Nefit Moduline 300)"}, + {EMS_MODEL_RC30, 78, 0x10, "RC30 (Nefit Moduline 400)"}, + {EMS_MODEL_RC35, 86, 0x10, "RC35"}, + {EMS_MODEL_EASY, 202, 0x18, "TC100 (Nefit Easy/CT100)"} }; + +/* + * Known thermostat types and their abilities + */ +const _Thermostat_Type Thermostat_Types[] = { + + {EMS_MODEL_RC20, EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC30, EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC35, EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_EASY, EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_ES73, EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO} + +}; + +// calculate sizes of arrays +uint8_t _Model_Types_max = ArraySize(Model_Types); // number of models uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types -const _EMS_Types EMS_Types[] = { +const _EMS_Type EMS_Types[] = { - // Command commands - {EMS_ID_NONE, EMS_TYPE_Version, "Version", _process_Version}, + // common + {EMS_MODEL_ALL, EMS_TYPE_Version, "Version", _process_Version}, // Boiler commands - {EMS_ID_BOILER, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", _process_UBAMonitorFast}, - {EMS_ID_BOILER, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", _process_UBAMonitorSlow}, - {EMS_ID_BOILER, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage}, - {EMS_ID_BOILER, EMS_TYPE_UBAParameterWW, "UBAParameterWW", _process_UBAParameterWW}, - {EMS_ID_BOILER, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", NULL}, - {EMS_ID_BOILER, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", NULL}, - {EMS_ID_BOILER, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", NULL}, - {EMS_ID_BOILER, EMS_TYPE_UBAMaintenanceStatusMessage, "UBAMaintenanceStatusMessage", NULL}, - - // Thermostat commands - // common - {EMS_ID_THERMOSTAT, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_ID_THERMOSTAT, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_ID_THERMOSTAT, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", _process_UBAMonitorFast}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", _process_UBAMonitorSlow}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage}, + {EMS_MODEL_UBA, EMS_TYPE_UBAParameterWW, "UBAParameterWW", _process_UBAParameterWW}, + {EMS_MODEL_UBA, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", _process_UBATotalUptimeMessage}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", NULL}, + {EMS_MODEL_UBA, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", NULL}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMaintenanceStatusMessage, "UBAMaintenanceStatusMessage", NULL}, // RC20 - {EMS_ID_THERMOSTAT, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, - {EMS_ID_THERMOSTAT, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, + {EMS_MODEL_RC20, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + {EMS_MODEL_RC20, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_MODEL_RC20, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, + {EMS_MODEL_RC20, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, + {EMS_MODEL_RC20, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, // RC30 - {EMS_ID_THERMOSTAT, EMS_TYPE_RC30Set, "RC30Set", _process_RC30Set}, - {EMS_ID_THERMOSTAT, EMS_TYPE_RC30StatusMessage, "RC30StatusMessage", _process_RC30StatusMessage}, + {EMS_MODEL_RC30, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + {EMS_MODEL_RC30, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_MODEL_RC30, EMS_TYPE_RC30Set, "RC30Set", _process_RC30Set}, + {EMS_MODEL_RC30, EMS_TYPE_RC30StatusMessage, "RC30StatusMessage", _process_RC30StatusMessage}, + {EMS_MODEL_RC30, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, + + // RC35 + {EMS_MODEL_RC30, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + {EMS_MODEL_RC30, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_MODEL_RC30, EMS_TYPE_RC30Set, "RC35Set", _process_RC35Set}, + {EMS_MODEL_RC30, EMS_TYPE_RC30StatusMessage, "RC35StatusMessage", _process_RC35StatusMessage}, + {EMS_MODEL_RC30, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, // Easy - {EMS_ID_THERMOSTAT, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage} + {EMS_MODEL_EASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, + {EMS_MODEL_EASY, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints} + }; uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types @@ -109,118 +133,130 @@ _EMS_Boiler EMS_Boiler; _EMS_Thermostat EMS_Thermostat; // 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, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4A, - 0x4C, 0x4E, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, 0x6C, 0x6E, 0x70, - 0x72, 0x74, 0x76, 0x78, 0x7A, 0x7C, 0x7E, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, 0x90, 0x92, 0x94, 0x96, - 0x98, 0x9A, 0x9C, 0x9E, 0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, 0xB4, 0xB6, 0xB8, 0xBA, 0xBC, - 0xBE, 0xC0, 0xC2, 0xC4, 0xC6, 0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6, 0xD8, 0xDA, 0xDC, 0xDE, 0xE0, 0xE2, - 0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE, 0xF0, 0xF2, 0xF4, 0xF6, 0xF8, 0xFA, 0xFC, 0xFE, 0x19, 0x1B, 0x1D, 0x1F, 0x11, - 0x13, 0x15, 0x17, 0x09, 0x0B, 0x0D, 0x0F, 0x01, 0x03, 0x05, 0x07, 0x39, 0x3B, 0x3D, 0x3F, 0x31, 0x33, 0x35, 0x37, - 0x29, 0x2B, 0x2D, 0x2F, 0x21, 0x23, 0x25, 0x27, 0x59, 0x5B, 0x5D, 0x5F, 0x51, 0x53, 0x55, 0x57, 0x49, 0x4B, 0x4D, - 0x4F, 0x41, 0x43, 0x45, 0x47, 0x79, 0x7B, 0x7D, 0x7F, 0x71, 0x73, 0x75, 0x77, 0x69, 0x6B, 0x6D, 0x6F, 0x61, 0x63, - 0x65, 0x67, 0x99, 0x9B, 0x9D, 0x9F, 0x91, 0x93, 0x95, 0x97, 0x89, 0x8B, 0x8D, 0x8F, 0x81, 0x83, 0x85, 0x87, 0xB9, - 0xBB, 0xBD, 0xBF, 0xB1, 0xB3, 0xB5, 0xB7, 0xA9, 0xAB, 0xAD, 0xAF, 0xA1, 0xA3, 0xA5, 0xA7, 0xD9, 0xDB, 0xDD, 0xDF, - 0xD1, 0xD3, 0xD5, 0xD7, 0xC9, 0xCB, 0xCD, 0xCF, 0xC1, 0xC3, 0xC5, 0xC7, 0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5, - 0xF7, 0xE9, 0xEB, 0xED, 0xEF, 0xE1, 0xE3, 0xE5, 0xE7}; +const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, + 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, + 0x48, 0x4A, 0x4C, 0x4E, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, + 0x6C, 0x6E, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7A, 0x7C, 0x7E, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, + 0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E, 0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, + 0xB4, 0xB6, 0xB8, 0xBA, 0xBC, 0xBE, 0xC0, 0xC2, 0xC4, 0xC6, 0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6, + 0xD8, 0xDA, 0xDC, 0xDE, 0xE0, 0xE2, 0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE, 0xF0, 0xF2, 0xF4, 0xF6, 0xF8, 0xFA, + 0xFC, 0xFE, 0x19, 0x1B, 0x1D, 0x1F, 0x11, 0x13, 0x15, 0x17, 0x09, 0x0B, 0x0D, 0x0F, 0x01, 0x03, 0x05, 0x07, + 0x39, 0x3B, 0x3D, 0x3F, 0x31, 0x33, 0x35, 0x37, 0x29, 0x2B, 0x2D, 0x2F, 0x21, 0x23, 0x25, 0x27, 0x59, 0x5B, + 0x5D, 0x5F, 0x51, 0x53, 0x55, 0x57, 0x49, 0x4B, 0x4D, 0x4F, 0x41, 0x43, 0x45, 0x47, 0x79, 0x7B, 0x7D, 0x7F, + 0x71, 0x73, 0x75, 0x77, 0x69, 0x6B, 0x6D, 0x6F, 0x61, 0x63, 0x65, 0x67, 0x99, 0x9B, 0x9D, 0x9F, 0x91, 0x93, + 0x95, 0x97, 0x89, 0x8B, 0x8D, 0x8F, 0x81, 0x83, 0x85, 0x87, 0xB9, 0xBB, 0xBD, 0xBF, 0xB1, 0xB3, 0xB5, 0xB7, + 0xA9, 0xAB, 0xAD, 0xAF, 0xA1, 0xA3, 0xA5, 0xA7, 0xD9, 0xDB, 0xDD, 0xDF, 0xD1, 0xD3, 0xD5, 0xD7, 0xC9, 0xCB, + 0xCD, 0xCF, 0xC1, 0xC3, 0xC5, 0xC7, 0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5, 0xF7, 0xE9, 0xEB, 0xED, 0xEF, + 0xE1, 0xE3, 0xE5, 0xE7}; -const uint8_t RX_READ_TIMEOUT_COUNT = 3; // 3 retries before timeout +const uint8_t TX_WRITE_TIMEOUT_COUNT = 3; // 3 retries before timeout -uint8_t emsRxRetryCount; // used for retries when sending failed +uint8_t emsTxRetryCount; // used for retries when sending failed // init stats and counters and buffers // uses -255 or 255 for values that haven't been set yet (EMS_VALUE_INT_NOTSET and EMS_VALUE_FLOAT_NOTSET) void ems_init() { // overall status - EMS_Sys_Status.emsRxPgks = 0; - EMS_Sys_Status.emsTxPkgs = 0; - EMS_Sys_Status.emxCrcErr = 0; - EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE; - EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; - EMS_Sys_Status.emsRefreshed = false; - EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled - EMS_Sys_Status.emsTxEnabled = true; // start up with Tx enabled - EMS_Sys_Status.emsThermostatEnabled = true; // there is a RCxx thermostat active as default - EMS_Sys_Status.emsBoilerEnabled = false; // boiler is not connected yet + EMS_Sys_Status.emsRxPgks = 0; + EMS_Sys_Status.emsTxPkgs = 0; + EMS_Sys_Status.emxCrcErr = 0; + EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE; + EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; + EMS_Sys_Status.emsRefreshed = false; + EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled + EMS_Sys_Status.emsTxEnabled = true; // start up with Tx enabled + EMS_Sys_Status.emsBusConnected = false; + + // no thermostat or boiler attached yet + ems_setThermostatEnabled(false); // there is a RCxx thermostat active as default + ems_setBoilerEnabled(false); // boiler is not connected yet EMS_Sys_Status.emsLogging = EMS_SYS_LOGGING_NONE; // Verbose logging is off // thermostat - EMS_Thermostat.type = EMS_ID_THERMOSTAT; // type, see my_config.h - EMS_Thermostat.hour = 0; - EMS_Thermostat.minute = 0; - EMS_Thermostat.second = 0; - EMS_Thermostat.day = 0; - EMS_Thermostat.month = 0; - EMS_Thermostat.year = 0; - EMS_Thermostat.mode = 255; // dummy value + EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_FLOAT_NOTSET; + EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET; + EMS_Thermostat.hour = 0; + EMS_Thermostat.minute = 0; + EMS_Thermostat.second = 0; + EMS_Thermostat.day = 0; + EMS_Thermostat.month = 0; + EMS_Thermostat.year = 0; + EMS_Thermostat.mode = 255; // dummy value + + EMS_Thermostat.type_id = EMS_ID_NONE; + EMS_Thermostat.model_id = EMS_MODEL_NONE; + EMS_Thermostat.read_supported = false; + EMS_Thermostat.write_supported = false; // UBAParameterWW EMS_Boiler.wWActivated = EMS_VALUE_INT_NOTSET; // Warm Water activated EMS_Boiler.wWSelTemp = EMS_VALUE_INT_NOTSET; // Warm Water selected temperature EMS_Boiler.wWCircPump = EMS_VALUE_INT_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; // UBAMonitorFast - EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow temperature - EMS_Boiler.curFlowTemp = EMS_VALUE_FLOAT_NOTSET; // Current flow temperature - EMS_Boiler.retTemp = EMS_VALUE_FLOAT_NOTSET; // Return temperature - EMS_Boiler.burnGas = EMS_VALUE_INT_NOTSET; // Gas on/off - EMS_Boiler.fanWork = EMS_VALUE_INT_NOTSET; // Fan on/off - EMS_Boiler.ignWork = EMS_VALUE_INT_NOTSET; // Ignition on/off - EMS_Boiler.heatPmp = EMS_VALUE_INT_NOTSET; // Boiler pump on/off - EMS_Boiler.wWHeat = EMS_VALUE_INT_NOTSET; // 3-way valve on WW - EMS_Boiler.wWCirc = EMS_VALUE_INT_NOTSET; // Circulation on/off - EMS_Boiler.selBurnPow = EMS_VALUE_INT_NOTSET; // Burner max power - EMS_Boiler.curBurnPow = EMS_VALUE_INT_NOTSET; // Burner current power - EMS_Boiler.flameCurr = EMS_VALUE_FLOAT_NOTSET; // Flame current in micro amps - EMS_Boiler.sysPress = EMS_VALUE_FLOAT_NOTSET; // System pressure + EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow temperature + EMS_Boiler.curFlowTemp = EMS_VALUE_FLOAT_NOTSET; // Current flow temperature + EMS_Boiler.retTemp = EMS_VALUE_FLOAT_NOTSET; // Return temperature + EMS_Boiler.burnGas = EMS_VALUE_INT_NOTSET; // Gas on/off + EMS_Boiler.fanWork = EMS_VALUE_INT_NOTSET; // Fan on/off + EMS_Boiler.ignWork = EMS_VALUE_INT_NOTSET; // Ignition on/off + EMS_Boiler.heatPmp = EMS_VALUE_INT_NOTSET; // Boiler pump on/off + EMS_Boiler.wWHeat = EMS_VALUE_INT_NOTSET; // 3-way valve on WW + EMS_Boiler.wWCirc = EMS_VALUE_INT_NOTSET; // Circulation on/off + EMS_Boiler.selBurnPow = EMS_VALUE_INT_NOTSET; // Burner max power + EMS_Boiler.curBurnPow = EMS_VALUE_INT_NOTSET; // Burner current power + EMS_Boiler.flameCurr = EMS_VALUE_FLOAT_NOTSET; // Flame current in micro amps + EMS_Boiler.sysPress = EMS_VALUE_FLOAT_NOTSET; // System pressure + EMS_Boiler.serviceCodeChar1 = EMS_VALUE_INT_NOTSET; // service codes + EMS_Boiler.serviceCodeChar2 = EMS_VALUE_INT_NOTSET; // service codes // UBAMonitorSlow EMS_Boiler.extTemp = EMS_VALUE_FLOAT_NOTSET; // Outside temperature EMS_Boiler.boilTemp = EMS_VALUE_FLOAT_NOTSET; // Boiler temperature EMS_Boiler.pumpMod = EMS_VALUE_INT_NOTSET; // Pump modulation - EMS_Boiler.burnStarts = EMS_VALUE_INT_NOTSET; // # burner restarts - EMS_Boiler.burnWorkMin = EMS_VALUE_INT_NOTSET; // Total burner operating time - EMS_Boiler.heatWorkMin = EMS_VALUE_INT_NOTSET; // Total heat operating time + EMS_Boiler.burnStarts = EMS_VALUE_LONG_NOTSET; // # burner restarts + EMS_Boiler.burnWorkMin = EMS_VALUE_LONG_NOTSET; // Total burner operating time + EMS_Boiler.heatWorkMin = EMS_VALUE_LONG_NOTSET; // Total heat operating time // UBAMonitorWWMessage EMS_Boiler.wWCurTmp = EMS_VALUE_FLOAT_NOTSET; // Warm Water current temperature: - EMS_Boiler.wWStarts = EMS_VALUE_INT_NOTSET; // Warm Water # starts - EMS_Boiler.wWWorkM = EMS_VALUE_INT_NOTSET; // Warm Water # minutes + EMS_Boiler.wWStarts = EMS_VALUE_LONG_NOTSET; // Warm Water # starts + EMS_Boiler.wWWorkM = EMS_VALUE_LONG_NOTSET; // Warm Water # minutes EMS_Boiler.wWOneTime = EMS_VALUE_INT_NOTSET; // Warm Water one time function on/off + EMS_Boiler.wWCurFlow = EMS_VALUE_INT_NOTSET; + + // UBATotalUptimeMessage + EMS_Boiler.UBAuptime = EMS_VALUE_LONG_NOTSET; // Total UBA working hours EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off EMS_Boiler.heatingActive = EMS_VALUE_INT_NOTSET; // Central heating is on/off + + EMS_Boiler.type_id = EMS_ID_NONE; + EMS_Boiler.model_id = EMS_MODEL_NONE; } // Getters and Setters for parameters void ems_setPoll(bool b) { EMS_Sys_Status.emsPollEnabled = b; - myDebug("EMS Bus Poll is set to %s\n", EMS_Sys_Status.emsPollEnabled ? "enabled" : "disabled"); + myDebug("EMS Bus Poll is set to %s", EMS_Sys_Status.emsPollEnabled ? "enabled" : "disabled"); } bool ems_getPoll() { return EMS_Sys_Status.emsPollEnabled; } -/** - * ! Getters and Setters for parameters - */ void ems_setTxEnabled(bool b) { EMS_Sys_Status.emsTxEnabled = b; - myDebug("EMS Bus Tx is set to %s\n", EMS_Sys_Status.emsTxEnabled ? "enabled" : "disabled"); + myDebug("EMS Bus Tx is set to %s", EMS_Sys_Status.emsTxEnabled ? "enabled" : "disabled"); } bool ems_getTxEnabled() { return EMS_Sys_Status.emsTxEnabled; } -bool ems_getBoilerEnabled() { - return EMS_Sys_Status.emsBoilerEnabled; -} - bool ems_getEmsRefreshed() { return EMS_Sys_Status.emsRefreshed; } @@ -229,13 +265,30 @@ void ems_setEmsRefreshed(bool b) { EMS_Sys_Status.emsRefreshed = b; } +bool ems_getBoilerEnabled() { + return EMS_Sys_Status.emsBoilerEnabled; +} + +void ems_setBoilerEnabled(bool b) { + EMS_Sys_Status.emsBoilerEnabled = b; + myDebug("Boiler set to %s", EMS_Sys_Status.emsBoilerEnabled ? "enabled" : "disabled"); +} + bool ems_getThermostatEnabled() { return EMS_Sys_Status.emsThermostatEnabled; } +bool ems_getBusConnected() { + return EMS_Sys_Status.emsBusConnected; +} + +void ems_setBusConnected(bool b) { + EMS_Sys_Status.emsBusConnected = b; +} + void ems_setThermostatEnabled(bool b) { EMS_Sys_Status.emsThermostatEnabled = b; - myDebug("Thermostat is set to %s\n", EMS_Sys_Status.emsThermostatEnabled ? "enabled" : "disabled"); + myDebug("Thermostat set to %s", EMS_Sys_Status.emsThermostatEnabled ? "enabled" : "disabled"); } _EMS_SYS_LOGGING ems_getLogging() { @@ -246,24 +299,19 @@ uint8_t ems_getEmsTypesCount() { return _EMS_Types_max; } -uint8_t ems_getThermostatTypesCount() { - return _Thermostat_Types_max; -} - void ems_setLogging(_EMS_SYS_LOGGING loglevel) { if (loglevel <= EMS_SYS_LOGGING_VERBOSE) { EMS_Sys_Status.emsLogging = loglevel; - myDebug("System Logging is set to "); if (loglevel == EMS_SYS_LOGGING_NONE) { - myDebug("None\n"); + myDebug("System Logging is set to None"); } else if (loglevel == EMS_SYS_LOGGING_BASIC) { - myDebug("Basic\n"); + myDebug("System Logging is set to Basic"); } else if (loglevel == EMS_SYS_LOGGING_VERBOSE) { - myDebug("Verbose\n"); + myDebug("System Logging is set to Verbose"); } else if (loglevel == EMS_SYS_LOGGING_THERMOSTAT) { - myDebug("Thermostat only\n"); + myDebug("System Logging is set to Thermostat only"); } else if (loglevel == EMS_SYS_LOGGING_RAW) { - myDebug("Raw mode\n"); + myDebug("System Logging is set to Raw mode"); } } } @@ -301,13 +349,13 @@ float _toFloat(uint8_t i, uint8_t * data) { int16_t x = (data[i] << 8) + data[i + 1]; return ((float)(x)) / 10; } else { - // positive number + // ...a positive number return ((float)(((data[i] << 8) + data[i + 1]))) / 10; } } // function to turn a telegram long (3 bytes) to a long int -uint16_t _toLong(uint8_t i, uint8_t * data) { +uint32_t _toLong(uint8_t i, uint8_t * data) { return (((data[i]) << 16) + ((data[i + 1]) << 8) + (data[i + 2])); } @@ -329,25 +377,72 @@ int _ems_findType(uint8_t type) { return (typeFound ? i : -1); } +// like itoa but for hex, and quick +char * _hextoa(uint8_t value, char * buffer) { + char * p = buffer; + byte nib1 = (value >> 4) & 0x0F; + byte nib2 = (value >> 0) & 0x0F; + *p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA; + *p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA; + *p = '\0'; // null terminate just in case + return buffer; +} + +// for decimals 0 to 99, printed as a string +char * _smallitoa(uint8_t value, char * buffer) { + if ((value / 10) == 0) { + buffer[0] = ((value / 10) == 0) ? '0' : (value / 10) + '0'; + } + buffer[1] = (value % 10) + '0'; + buffer[2] = '\0'; + return buffer; +} + + /** - * debug print a telegram to telnet console + * debug print a telegram to telnet/serial including the CRC * len is length in bytes including the CRC */ void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const char * color) { if (EMS_Sys_Status.emsLogging <= EMS_SYS_LOGGING_BASIC) return; - myDebug("%s%s telegram: ", color, prefix); + char output_str[300] = {0}; // roughly EMS_MAX_TELEGRAM_LENGTH*3 + 20 + char buffer[16] = {0}; + + unsigned long upt = millis(); + strlcpy(output_str, "(", sizeof(output_str)); + strlcat(output_str, COLOR_CYAN, sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((upt / 3600000) % 24), buffer), sizeof(output_str)); + strlcat(output_str, ":", sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((upt / 60000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, ":", sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((upt / 1000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, COLOR_RESET, sizeof(output_str)); + strlcat(output_str, ") ", sizeof(output_str)); + + strlcat(output_str, color, sizeof(output_str)); + strlcat(output_str, prefix, sizeof(output_str)); + strlcat(output_str, " telegram: ", sizeof(output_str)); + for (int i = 0; i < len - 1; i++) { - myDebug("%02X ", data[i]); + strlcat(output_str, _hextoa(data[i], buffer), sizeof(output_str)); + strlcat(output_str, " ", sizeof(output_str)); // add space } - myDebug("(CRC=%02X", data[len - 1]); + + strlcat(output_str, "(CRC=", sizeof(output_str)); + strlcat(output_str, _hextoa(data[len - 1], buffer), sizeof(output_str)); + strlcat(output_str, ")", sizeof(output_str)); // print number of data bytes only if its a valid telegram if (len > 5) { - myDebug(", #data=%d", (len - 5)); + strlcat(output_str, ", #data=", sizeof(output_str)); + strlcat(output_str, itoa(len - 5, buffer, 10), sizeof(output_str)); } - myDebug(")%s\n", COLOR_RESET); + + strlcat(output_str, COLOR_RESET, sizeof(output_str)); + + myDebug(output_str); } /** @@ -366,9 +461,8 @@ void _ems_sendTelegram() { // if we're in raw mode just fire and forget if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_RAW) { - EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = - _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC - _debugPrintTelegram("Sending raw", EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); // always show + EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC + _debugPrintTelegram("Sending raw", EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); // always show emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; // finished sending EMS_TxQueue.shift(); // remove from queue @@ -377,7 +471,7 @@ void _ems_sendTelegram() { // if Tx is disabled, don't do anything and ignore the request if (!EMS_Sys_Status.emsTxEnabled) { - myDebug("Tx is disabled. Ignoring %s request to 0x%02X.\n", + myDebug("Tx is disabled. Ignoring %s request to 0x%02X.", ((EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) ? "write" : "read"), EMS_TxTelegram.dest & 0x7F); EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; // finished sending @@ -385,13 +479,6 @@ void _ems_sendTelegram() { return; } - // if this telegram has already been processed then skip it - // leave on queue until its processed later on - if (EMS_TxTelegram.hasSent) { - // myDebug("Already sent!"); - return; - } - EMS_Sys_Status.emsTxStatus = EMS_TX_ACTIVE; // create header @@ -418,13 +505,13 @@ void _ems_sendTelegram() { // print debug info if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - char s[64]; + char s[64] = {0}; if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { - sprintf(s, "Sending write of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); + snprintf(s, sizeof(s), "Sending write of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) { - sprintf(s, "Sending read of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); + snprintf(s, sizeof(s), "Sending read of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { - sprintf(s, "Sending validate of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); + snprintf(s, sizeof(s), "Sending validate of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } _debugPrintTelegram(s, EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); @@ -435,9 +522,6 @@ void _ems_sendTelegram() { EMS_Sys_Status.emsTxPkgs++; - // dirty hack. we really shouldn't be changing values in the buffer directly. - EMS_TxTelegram.hasSent = true; - // if it was a write command, check if we need to do a new read to validate the results // we do this by turning the last write into a read if ((EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) && (EMS_TxTelegram.type_validate != EMS_ID_NONE)) { @@ -445,17 +529,16 @@ void _ems_sendTelegram() { _EMS_TxTelegram new_EMS_TxTelegram; // copy details - new_EMS_TxTelegram.type_validate = EMS_TxTelegram.type_validate; - new_EMS_TxTelegram.dest = EMS_TxTelegram.dest; - new_EMS_TxTelegram.type = EMS_TxTelegram.type; - new_EMS_TxTelegram.action = EMS_TX_TELEGRAM_VALIDATE; - new_EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // location of byte to fetch - new_EMS_TxTelegram.dataValue = 1; // fetch single byte - new_EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; // is always 6 bytes long (including CRC at end) - new_EMS_TxTelegram.comparisonValue = EMS_TxTelegram.comparisonValue; + new_EMS_TxTelegram.type_validate = EMS_TxTelegram.type_validate; + new_EMS_TxTelegram.dest = EMS_TxTelegram.dest; + new_EMS_TxTelegram.type = EMS_TxTelegram.type; + new_EMS_TxTelegram.action = EMS_TX_TELEGRAM_VALIDATE; + new_EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // location of byte to fetch + new_EMS_TxTelegram.dataValue = 1; // fetch single byte + new_EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; // is always 6 bytes long (including CRC at end) + new_EMS_TxTelegram.comparisonValue = EMS_TxTelegram.comparisonValue; new_EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.comparisonPostRead; new_EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.comparisonOffset; - new_EMS_TxTelegram.hasSent = false; // remove old telegram from queue and add this new read one EMS_TxQueue.shift(); // remove from queue @@ -480,8 +563,8 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // check first for a Poll for us if (value == (EMS_ID_ME | 0x80)) { - // set the timestamp of the last poll, we use this to see if we have a connection to the boiler - EMS_Sys_Status.emsBoilerEnabled = true; + // we use this to see if we always have a connection to the boiler, in case of drop outs + EMS_Sys_Status.emsBusConnected = true; // do we have something to send thats waiting in the Tx queue? if so send it if (!EMS_TxQueue.isEmpty()) { @@ -495,14 +578,14 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { } else if ((value == EMS_TX_ERROR) || (value == EMS_TX_SUCCESS)) { // if its a success (01) or failure (04), then see if its from one of our last writes // a response from UBA after a write should be within a specific time period < 100ms - // TODO what we should really do here is just cancel the write operation + // What we should really do here is just cancel the write operation (for later!) if (!EMS_TxQueue.isEmpty()) { _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); // get current Tx package we last sent if ((EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) && (value == EMS_TX_ERROR)) { if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - myDebug("** Error: last write failed. removing write op from queue!\n"); + myDebug("* Error: last write failed. removing write request from queue!"); } - EMS_TxQueue.shift(); // write failed so remove from queue. pretty sloppy. + EMS_TxQueue.shift(); // write failed so remove from queue and forget it for now } emsaurt_tx_poll(); // send a poll to free the EMS bus } @@ -526,19 +609,36 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { if (telegram[length - 1] != crc) { EMS_Sys_Status.emxCrcErr++; _debugPrintTelegram("Corrupt telegram:", telegram, length, COLOR_RED); - } else { - // if we in raw mode then just output the telegram - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW) { - for (int i = 0; i < length; i++) { - myDebug("%02X ", telegram[i]); + // at this point something arrived on the bus, which means if we were waiting for something to arrive then we + // can forget it as it should have arrived in a 100ms window. So remove it. + if (!EMS_TxQueue.isEmpty()) { + /* + // commented out because too much chatter + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Corrupt telegram, so removing last read from Tx queue."); } - myDebug("\n"); + */ + EMS_TxQueue.shift(); } - - // here we know its a valid incoming telegram of at least 6 bytes - // lets process it and see what to do next - _processType(telegram, length); + return; } + + // if we are in raw logging mode then just print out the telegram as it is + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW) { + char raw[300] = {0}; + char buffer[16] = {0}; + for (int i = 0; i < length; i++) { + strlcat(raw, _hextoa(telegram[i], buffer), sizeof(raw)); + strlcat(raw, " ", sizeof(raw)); // add space + //snprintf(s, sizeof(s), "%02X ", telegram[i]); + //raw += s; + } + myDebug(raw); + } + + // here we know its a valid incoming telegram of at least 6 bytes + // lets process it and see what to do next + _processType(telegram, length); } /** @@ -554,77 +654,83 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { // print detailed telegram data if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { - char color_s[20]; - char src_s[20]; - char dest_s[20]; - char s[100]; + char output_str[300] = {0}; // roughly EMS_MAX_TELEGRAM_LENGTH*3 + 20 + char buffer[16] = {0}; + char color_s[20] = {0}; - // set source string - if (src == EMS_ID_BOILER) { - strcpy(src_s, "Boiler -> "); - } else if (src == EMS_ID_THERMOSTAT) { - strcpy(src_s, "Thermostat -> "); + // source + if (src == EMS_Boiler.type_id) { + strlcpy(output_str, "Boiler", sizeof(output_str)); + } else if (src == EMS_Thermostat.type_id) { + strlcpy(output_str, "Thermostat", sizeof(output_str)); } else { - sprintf(src_s, "0x%02X -> ", src); + strlcpy(output_str, "0x", sizeof(output_str)); + strlcat(output_str, _hextoa(src, buffer), sizeof(output_str)); } - // set destination string + strlcat(output_str, " -> ", sizeof(output_str)); + + // destination if (dest == EMS_ID_ME) { - strcpy(dest_s, "me"); - strcpy(color_s, COLOR_YELLOW); + strlcat(output_str, "me", sizeof(output_str)); + strlcpy(color_s, COLOR_YELLOW, sizeof(color_s)); } else if (dest == EMS_ID_NONE) { - // it's probably just a broadcast - strcpy(dest_s, "all"); - strcpy(color_s, COLOR_GREEN); - } else if (dest == EMS_ID_BOILER) { - strcpy(dest_s, "Boiler"); - strcpy(color_s, COLOR_MAGENTA); - } else if (dest == EMS_ID_THERMOSTAT) { - strcpy(dest_s, "Thermostat"); - strcpy(color_s, COLOR_MAGENTA); + strlcat(output_str, "all", sizeof(output_str)); + strlcpy(color_s, COLOR_GREEN, sizeof(color_s)); + } else if (dest == EMS_Boiler.type_id) { + strlcat(output_str, "Boiler", sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + } else if (dest == EMS_Thermostat.type_id) { + strlcat(output_str, "Thermostat", sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); } else { - sprintf(dest_s, "0x%02X", dest); - strcpy(color_s, COLOR_MAGENTA); + strlcat(output_str, "0x", sizeof(output_str)); + strlcat(output_str, _hextoa(dest, buffer), sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); } - sprintf(s, "%s%s, type 0x%02X", src_s, dest_s, type); - // and print telegram + // type + strlcat(output_str, ", type 0x", sizeof(output_str)); + strlcat(output_str, _hextoa(type, buffer), 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_ID_THERMOSTAT) || (dest == EMS_ID_THERMOSTAT)) { - _debugPrintTelegram(s, telegram, length, color_s); + if ((src == EMS_Thermostat.type_id) || (dest == EMS_Thermostat.type_id)) { + _debugPrintTelegram(output_str, telegram, length, color_s); } } else { // allways print - _debugPrintTelegram(s, telegram, length, color_s); + _debugPrintTelegram(output_str, telegram, length, color_s); } } - // try and match it against known types and call the call handler function - // only process telegrams broadcasting to everyone or sent to us specifically - if ((dest == EMS_ID_ME) || (dest == EMS_ID_NONE)) { - int i = 0; - bool typeFound = false; - while (i < _EMS_Types_max) { - if (((EMS_Types[i].src == src) || (EMS_Types[i].src == EMS_ID_NONE)) && (EMS_Types[i].type == type)) { - // we have a match - typeFound = true; - // call callback to fetch the values from the telegram - // data block is sent, which starts with the 5th byte of the telegram - // return value tells us if we need to force send values back to MQTT - // the length is the #bytes of the data (excluding the header and CRC) - if ((EMS_Types[i].processType_cb) != (void *)NULL) { - // print non-verbose message - if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) - || (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE)) { - myDebug("<--- %s(0x%02X) received\n", EMS_Types[i].typeString, type); - } - (void)EMS_Types[i].processType_cb(data, length - 5); - } - break; + // see if we recognize the type first by scanning our known EMS types list + // trying to match the type ID + bool commonType = false; + bool typeFound = false; + bool forUs = false; + int i = 0; + + while (i < _EMS_Types_max) { + if (EMS_Types[i].type == type) { + typeFound = true; + commonType = (EMS_Types[i].model_id == EMS_MODEL_ALL); // is it common type for everyone? + forUs = (src == EMS_Boiler.type_id) || (src == EMS_Thermostat.type_id); // is it for us? So the src must match our own id + break; + } + i++; + } + + // if it's a common type (across ems devices) or something specifically for us process it. + // dest will be EMS_ID_NONE for a broadcast message + if (typeFound && (commonType || forUs)) { + if ((EMS_Types[i].processType_cb) != (void *)NULL) { + // print non-verbose message + if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) || (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE)) { + myDebug("<--- %s(0x%02X) received", EMS_Types[i].typeString, type); } - i++; + // call callback function to process it + (void)EMS_Types[i].processType_cb(data, length - 5); } } } @@ -648,8 +754,8 @@ void _processType(uint8_t * telegram, uint8_t length) { return; } - // did we request this telegram? If so it would be a read or a validate telegram still on the Tx queue - // with the same type + // did we request this telegram? If so it would be either a read or a validate telegram and still on the + // Tx queue with the same type // if its a validate check the value, or if its a read, update the Read counter // then we can safely removed the read/validate from the queue if ((dest == EMS_ID_ME) && (!EMS_TxQueue.isEmpty())) { @@ -659,11 +765,11 @@ void _processType(uint8_t * telegram, uint8_t length) { if (EMS_TxTelegram.type == type) { emsaurt_tx_poll(); // send Acknowledgement back to free the EMS bus - // if last action was a read, where just happy that we got a response back + // if last action was a read, we are just happy that we actually got a response back if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) { EMS_Sys_Status.emsRxPgks++; // increment rx counter - emsRxRetryCount = 0; // reset retry count - _ems_processTelegram(telegram, length); // and process it + emsTxRetryCount = 0; // reset retry count + _ems_processTelegram(telegram, length); // and process the telegram if (EMS_TxTelegram.forceRefresh) { ems_setEmsRefreshed(true); // set the MQTT refresh flag to force sending to MQTT } @@ -675,7 +781,7 @@ void _processType(uint8_t * telegram, uint8_t length) { // there is a match, so write must have been successful EMS_TxQueue.shift(); // remove validate from queue if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Write to 0x%02X successful.\n", EMS_TxTelegram.dest); + myDebug("Write to 0x%02X successful.", EMS_TxTelegram.dest); } ems_doReadCommand(EMS_TxTelegram.comparisonPostRead, EMS_TxTelegram.dest, @@ -683,34 +789,35 @@ void _processType(uint8_t * telegram, uint8_t length) { } else { // write failed. if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Last write failed. Compared set value 0x%02X with received value 0x%02X. ", + myDebug("Last write failed. Compared set value 0x%02X with received value 0x%02X.", EMS_TxTelegram.comparisonValue, dataReceived); } - if (emsRxRetryCount++ >= RX_READ_TIMEOUT_COUNT) { + if (emsTxRetryCount++ >= TX_WRITE_TIMEOUT_COUNT) { // give up if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Giving up!\n"); + myDebug("...Giving up!"); } EMS_TxQueue.shift(); // remove from queue } else { // retry, turn the validate back into a write and try again if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Retrying attempt %d...\n", emsRxRetryCount); + myDebug("...Retrying attempt %d...", emsTxRetryCount); } EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; EMS_TxTelegram.dataValue = EMS_TxTelegram.comparisonValue; // restore old value EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // restore old value EMS_TxQueue.shift(); // remove validate from queue - EMS_TxQueue.unshift(EMS_TxTelegram); // add back to queue making it next in line + EMS_TxQueue.unshift(EMS_TxTelegram); // add back to queue making it next in line } } } } // telegram was for us, but seems we didn't ask for it + // ** do nothing ** } else { - // we didn't request it - _ems_processTelegram(telegram, length); // and process and print it + // we didn't request it, was for somebody else, print it out anyway + _ems_processTelegram(telegram, length); } } @@ -721,14 +828,17 @@ void _processType(uint8_t * telegram, uint8_t length) { * tap water is on if Selected Flow Temp = 0 and Selected Burner Power >= 115 */ bool _checkActive() { - // hot tap water - EMS_Boiler.tapwaterActive = - ((EMS_Boiler.selFlowTemp == 0) - && (EMS_Boiler.selBurnPow >= EMS_BOILER_BURNPOWER_TAPWATER) & (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + /* + EMS_Boiler.tapwaterActive = ((EMS_Boiler.selFlowTemp == 0) + && (EMS_Boiler.selBurnPow >= EMS_BOILER_BURNPOWER_TAPWATER) & (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + + */ + + // hot tap water, using flow to check insread of the burner power + EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); // heating - EMS_Boiler.heatingActive = - ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + EMS_Boiler.heatingActive = ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); } /** @@ -740,9 +850,18 @@ void _process_UBAParameterWW(uint8_t * data, uint8_t length) { EMS_Boiler.wWSelTemp = data[2]; EMS_Boiler.wWCircPump = (data[6] == 0xFF); // 0xFF means on EMS_Boiler.wWDesiredTemp = data[8]; + EMS_Boiler.wWComfort = (data[EMS_OFFSET_UBAParameterWW_wwComfort] == 0x00); - // when we receieve this, lets force an MQTT publish - EMS_Sys_Status.emsRefreshed = true; + EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish +} + +/** + * UBATotalUptimeMessage - type 0x14 - total uptime + * received only after requested (not broadcasted) + */ +void _process_UBATotalUptimeMessage(uint8_t * data, uint8_t length) { + EMS_Boiler.UBAuptime = _toLong(0, data); + EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish } /** @@ -754,6 +873,7 @@ void _process_UBAMonitorWWMessage(uint8_t * data, uint8_t length) { EMS_Boiler.wWStarts = _toLong(13, data); EMS_Boiler.wWWorkM = _toLong(10, data); EMS_Boiler.wWOneTime = bitRead(data[5], 1); + EMS_Boiler.wWCurFlow = data[9]; } /** @@ -778,6 +898,10 @@ void _process_UBAMonitorFast(uint8_t * data, uint8_t length) { EMS_Boiler.flameCurr = _toFloat(15, data); + // read the service code / installation status as appears on the display + EMS_Boiler.serviceCodeChar1 = data[18]; // ascii character 1 + EMS_Boiler.serviceCodeChar2 = data[19]; // ascii character 2 + if (data[17] == 0xFF) { // missing value for system pressure EMS_Boiler.sysPress = 0; } else { @@ -825,6 +949,18 @@ void _process_RC30StatusMessage(uint8_t * data, uint8_t length) { EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT } +/** + * RC35StatusMessage - type 0x3E - data from the RC35 thermostat (0x10) + * For reading the temp values only + * received every 60 seconds + */ +void _process_RC35StatusMessage(uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = ((float)data[2]) / (float)2; + EMS_Thermostat.curr_roomTemp = _toFloat(3, data); + + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT +} + /** * EasyStatusMessage - type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long * The Easy has a digital precision of its floats to 2 decimal places, so values is divided by 100 @@ -852,6 +988,14 @@ void _process_RC30Set(uint8_t * data, uint8_t length) { EMS_Thermostat.mode = data[EMS_OFFSET_RC30Set_mode]; } +/** + * RC35Temperature - type 0x3D - for reading the mode from the RC35 thermostat (0x10) + * received only after requested + */ +void _process_RC35Set(uint8_t * data, uint8_t length) { + EMS_Thermostat.mode = data[EMS_OFFSET_RC35Set_mode]; +} + /** * RCOutdoorTempMessage - type 0xA3 - for external temp settings from the the RC* thermostats */ @@ -860,28 +1004,86 @@ void _process_RCOutdoorTempMessage(uint8_t * data, uint8_t length) { } /** - * Version - type 0x02 - get the firmware version and type of a EMS device (Boiler, Thermostat etc) - * When a thermostat is connecting it will send out 0x02 messages too, which we'll ignore - * We don't bother storing these values anywhere, just print them for now - * Moduline 300, Type 77. Version 3.03 - * Moduline 400, Type 78, Version 3.03 - * Nefit Easy = Type 202. Version 2.19 - * Nefit Trendline HRC30 = Type 123. Version 6.1 + * Version - type 0x02 - get the firmware version and type of a EMS device */ void _process_Version(uint8_t * data, uint8_t length) { // ignore short messages that we can't interpret - if (length >= 3) { - uint8_t type = data[0]; - uint8_t major = data[1]; - uint8_t minor = data[2]; - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Product ID %d. Version %02d.%02d\n", type, major, minor); + if (length < 3) { + return; + } + + uint8_t product_id = data[0]; + uint8_t major = data[1]; + uint8_t minor = data[2]; + int i = 0; + int j = 0; + bool typeFound = false; + bool isThermostat = false; + char version[10] = {0}; + snprintf(version, sizeof(version), "%02d.%02d", major, minor); + + while (i < _Model_Types_max) { + if (Model_Types[i].product_id == product_id) { + typeFound = true; // we have a matching product id + break; } + i++; + } + + if (!typeFound) { + myDebug("Unknown device found. Product ID %d, Version %s", product_id, version); + return; + } + + // check to see if its a thermostat + while (j < _Thermostat_Types_max) { + if (Thermostat_Types[j].model_id == Model_Types[i].model_id) { + isThermostat = true; // we have a matching model + break; + } + j++; + } + + // see if its a thermostat + if ((isThermostat) && (!EMS_Sys_Status.emsThermostatEnabled)) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Found a Thermostat. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + Model_Types[i].model_string, + Model_Types[i].type_id, + product_id, + version); + } + // set its capabilities + EMS_Thermostat.model_id = Model_Types[i].model_id; + EMS_Thermostat.type_id = Model_Types[i].type_id; + EMS_Thermostat.read_supported = Thermostat_Types[j].read_supported; + EMS_Thermostat.write_supported = Thermostat_Types[j].write_supported; + strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); + + ems_setThermostatEnabled(true); + ems_getThermostatValues(); // get Thermostat values (if supported) + } + + // assume its a boiler + if ((!isThermostat) && (!EMS_Sys_Status.emsBoilerEnabled)) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Found a Boiler compatible device, model %s with TypeID 0x%02X, Product ID %d, Version %s", + Model_Types[i].model_string, + Model_Types[i].type_id, + product_id, + version); + } + EMS_Boiler.type_id = Model_Types[i].type_id; + EMS_Boiler.model_id = Model_Types[i].model_id; + strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); + + ems_setBoilerEnabled(true); + ems_getBoilerValues(); // get Boiler values that we would usually have to wait for } } /** - * UBASetPoint 0x1A, for RC20 + * UBASetPoint 0x1A, for RC20 and other thermostats. not really sure what to do with this data yet. */ void _process_SetPoints(uint8_t * data, uint8_t length) { if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { @@ -889,9 +1091,8 @@ void _process_SetPoints(uint8_t * data, uint8_t length) { uint8_t setpoint = data[0]; uint8_t hk_power = data[1]; uint8_t ww_power = data[2]; - myDebug(" SetPoint=%d, hk_power=%d ww_power=%d\n", setpoint, hk_power, ww_power); + myDebug(" SetPoint=%d, hk_power=%d ww_power=%d", setpoint, hk_power, ww_power); } - myDebug("\n"); } } @@ -900,7 +1101,7 @@ void _process_SetPoints(uint8_t * data, uint8_t length) { * common for all thermostats */ void _process_RCTime(uint8_t * data, uint8_t length) { - if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_EASY) { + if (EMS_Thermostat.model_id == EMS_MODEL_EASY) { return; // not supported } @@ -912,6 +1113,7 @@ void _process_RCTime(uint8_t * data, uint8_t length) { EMS_Thermostat.year = data[0]; // we can optional set the time based on the thermostat's time if we want. + // make sure you include Time library and TimeLib.h if enabling this /* setTime(EMS_Thermostat.hour, EMS_Thermostat.minute, @@ -927,26 +1129,35 @@ void _process_RCTime(uint8_t * data, uint8_t length) { */ void ems_printTxQueue() { _EMS_TxTelegram EMS_TxTelegram; - char sType[20]; + char sType[20] = {0}; - myDebug("Tx queue (%d/%d)\n", EMS_TxQueue.size(), EMS_TxQueue.capacity); + myDebug("Tx queue (%d/%d)", EMS_TxQueue.size(), EMS_TxQueue.capacity); for (byte i = 0; i < EMS_TxQueue.size(); i++) { EMS_TxTelegram = EMS_TxQueue[i]; // retrieves the i-th element from the buffer without removing it // get action if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { - strcpy(sType, "write"); + strlcpy(sType, "write", sizeof(sType)); } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) { - strcpy(sType, "read"); + strlcpy(sType, "read", sizeof(sType)); } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { - strcpy(sType, "validate"); + strlcpy(sType, "validate", sizeof(sType)); } else { - strcpy(sType, "?"); + strlcpy(sType, "?", sizeof(sType)); } + char addedTime[15] = {0}; + unsigned long upt = EMS_TxTelegram.timestamp; + snprintf(addedTime, + sizeof(addedTime), + "(%02d:%02d:%02d)", + (uint8_t)((upt / (1000 * 60 * 60)) % 24), + (uint8_t)((upt / (1000 * 60)) % 60), + (uint8_t)((upt / 1000) % 60)); + myDebug(" [%d] action=%s dest=0x%02x type=0x%02x offset=%d length=%d dataValue=%d " - "comparisonValue=%d hasSent=%d, type_validate=0x%02x comparisonPostRead=0x%02x\n", + "comparisonValue=%d type_validate=0x%02x comparisonPostRead=0x%02x @ %s", i, sType, EMS_TxTelegram.dest & 0x7F, @@ -955,45 +1166,135 @@ void ems_printTxQueue() { EMS_TxTelegram.length, EMS_TxTelegram.dataValue, EMS_TxTelegram.comparisonValue, - EMS_TxTelegram.hasSent, EMS_TxTelegram.type_validate, - EMS_TxTelegram.comparisonPostRead); + EMS_TxTelegram.comparisonPostRead, + addedTime); } } /** - * Generic function to return temperature settings from the thermostat - * Supports RC20, RC30 and Easy + * Generic function to return various settings from the thermostat */ void ems_getThermostatValues() { - if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_RC20) { - ems_doReadCommand(EMS_TYPE_RC20StatusMessage, EMS_ID_THERMOSTAT); // to get the setpoint temp - ems_doReadCommand(EMS_TYPE_RC20Set, EMS_ID_THERMOSTAT); // to get the mode - } else if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_RC30) { - ems_doReadCommand(EMS_TYPE_RC30StatusMessage, EMS_ID_THERMOSTAT); // to get the setpoint temp - ems_doReadCommand(EMS_TYPE_RC30Set, EMS_ID_THERMOSTAT); // to get the mode - } else if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_EASY) { - ems_doReadCommand(EMS_TYPE_EasyStatusMessage, EMS_ID_THERMOSTAT); + if (!EMS_Sys_Status.emsThermostatEnabled) { + return; } + + if (!EMS_Thermostat.read_supported) { + myDebug("Read operations not yet supported for this model Thermostat"); + return; + } + + _EMS_MODEL_ID model_id = EMS_Thermostat.model_id; + uint8_t type = EMS_Thermostat.type_id; + + if (model_id == EMS_MODEL_RC20) { + ems_doReadCommand(EMS_TYPE_RC20StatusMessage, type); // to get the setpoint temp + ems_doReadCommand(EMS_TYPE_RC20Set, type); // to get the mode + } else if (model_id == EMS_MODEL_RC30) { + ems_doReadCommand(EMS_TYPE_RC30StatusMessage, type); // to get the setpoint temp + ems_doReadCommand(EMS_TYPE_RC30Set, type); // to get the mode + } else if (model_id == EMS_MODEL_EASY) { + ems_doReadCommand(EMS_TYPE_EasyStatusMessage, type); + } + + ems_doReadCommand(EMS_TYPE_RCTime, type); // get Thermostat time } /** - * print out current thermostat type + * Generic function to return various settings from the thermostat */ -void ems_printThermostatType() { - int i = 0; - bool typeFound = false; - while (i < _Thermostat_Types_max) { - if (Thermostat_Types[i].id == EMS_ID_THERMOSTAT) { - typeFound = true; // we have a match +void ems_getBoilerValues() { + ems_doReadCommand(EMS_TYPE_UBAMonitorFast, EMS_Boiler.type_id); // get boiler stats, instead of waiting 10secs for the broadcast + ems_doReadCommand(EMS_TYPE_UBAMonitorSlow, EMS_Boiler.type_id); // get more boiler stats, instead of waiting 60secs for the broadcast + ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_Boiler.type_id); // get Warm Water values + ems_doReadCommand(EMS_TYPE_UBATotalUptimeMessage, EMS_Boiler.type_id); // get Warm Water values +} + + +// return pointer to Model details +int _ems_findModel(_EMS_MODEL_ID model_id) { + uint8_t i = 0; + bool found = false; + + // scan through known ID types + while (i < _Model_Types_max) { + if (Model_Types[i].model_id == model_id) { + found = true; // we have a match break; } i++; } - if (typeFound) { - myDebug("%s [ID 0x%02X]", Thermostat_Types[i].typeString, Thermostat_Types[i].id); + if (!found) { + return -1; + } + + return i; +} + +char * _ems_buildModelString(char * buffer, uint8_t size, _EMS_MODEL_ID model_id) { + int i = _ems_findModel(model_id); + if (i != -1) { + char tmp[6] = {0}; + strlcpy(buffer, Model_Types[i].model_string, size); + strlcat(buffer, " [TypeID 0x", size); + strlcat(buffer, _hextoa(Model_Types[i].type_id, tmp), size); + strlcat(buffer, "] Product ID:", size); + strlcat(buffer, itoa(Model_Types[i].product_id, tmp, 10), size); } else { - myDebug("Unknown? [ID 0x%02X]", Thermostat_Types[i].id); + strlcpy(buffer, "", sizeof(buffer)); + } + + return buffer; +} + +/** + * returns current thermostat type as a string + */ +char * ems_getThermostatType(char * buffer) { + uint8_t size = 64; + if (!EMS_Sys_Status.emsThermostatEnabled) { + strlcpy(buffer, "", size); + } else { + _ems_buildModelString(buffer, size, EMS_Thermostat.model_id); + } + return buffer; +} + +/** + * returns current boiler type as a string + */ +char * ems_getBoilerType(char * buffer) { + uint8_t size = 64; + if (!EMS_Sys_Status.emsBoilerEnabled) { + strlcpy(buffer, "", size); + } else { + _ems_buildModelString(buffer, size, EMS_Boiler.model_id); + } + return buffer; +} + +// returns the model type for a thermostat +_EMS_MODEL_ID ems_getThermostatModel() { + if (EMS_Sys_Status.emsThermostatEnabled) { + return (EMS_Thermostat.model_id); + } else { + return EMS_MODEL_NONE; + } +} + +/* + * Find the versions of our connected devices + */ +void ems_getVersions() { + // send Version request to all known EMS devices + ems_setThermostatEnabled(false); + ems_setBoilerEnabled(false); + myDebug("Scanning EMS bus for devices. This may take a few seconds."); + for (int i = 0; i < _Model_Types_max; i++) { + if ((Model_Types[i].model_id != EMS_MODEL_NONE) && (Model_Types[i].model_id != EMS_MODEL_SERVICEKEY)) { + ems_doReadCommand(EMS_TYPE_Version, Model_Types[i].type_id); + } } } @@ -1001,25 +1302,36 @@ void ems_printThermostatType() { * Print out all handled types */ void ems_printAllTypes() { - myDebug("These %d telegram type IDs are recognized:\n", _EMS_Types_max); + myDebug("These %d telegram TypeIDs are recognized currently:", _EMS_Types_max); uint8_t i; - char s[20]; for (i = 0; i < _EMS_Types_max; i++) { - if (EMS_Types[i].src == EMS_ID_THERMOSTAT) { - strcpy(s, "Thermostat"); - } else if (EMS_Types[i].src == EMS_ID_BOILER) { - strcpy(s, "Boiler"); + if (EMS_Types[i].model_id == EMS_MODEL_NONE) { + myDebug(" (all devices) : type %02X (%s)", EMS_Types[i].type, EMS_Types[i].typeString); } else { - strcpy(s, "Common"); + int index = _ems_findModel(EMS_Types[i].model_id); + if (index != -1) { + myDebug(" %s : type %02X (%s)", Model_Types[index].model_string, EMS_Types[i].type, EMS_Types[i].typeString); + } } - myDebug(" %s:\ttype ID %02X (%s)\n", s, EMS_Types[i].type, EMS_Types[i].typeString); } - myDebug("\nThese %d telegram Thermostats are natively supported:\n", _Thermostat_Types_max); - + myDebug("\nThese %d thermostats are natively supported:", _Thermostat_Types_max); for (i = 0; i < _Thermostat_Types_max; i++) { - myDebug(" %s [ID 0x%02X]\n", Thermostat_Types[i].typeString, Thermostat_Types[i].id); + // find the model's details + for (int j = 0; j < _Model_Types_max; j++) { + if (Model_Types[j].model_id == Thermostat_Types[i].model_id) { + int index = _ems_findModel(Model_Types[j].model_id); + if (index != -1) { + myDebug(" %s [ID 0x%02X] Product ID:%d read_supported:%s write_supported:%s", + Model_Types[index].model_string, + Model_Types[index].type_id, + Model_Types[index].product_id, + (Thermostat_Types[i].read_supported) ? "yes" : "no", + (Thermostat_Types[i].write_supported) ? "yes" : "no"); + } + } + } } } @@ -1028,21 +1340,28 @@ void ems_printAllTypes() { * Read commands when sent must respond by the destination (target) immediately (or within 10ms) */ void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh) { - // if not a valid type of boiler is not accessible then quit - if ((type == EMS_ID_NONE) || (!EMS_Sys_Status.emsBoilerEnabled)) { + // if not a valid type of boiler is not accessible then quits + if (type == EMS_ID_NONE) { return; } + if (!EMS_Sys_Status.emsBoilerEnabled) { + if ((ems_getLogging() == EMS_SYS_LOGGING_VERBOSE)) { + myDebug("Boiler not initialized, queuing the read request."); + } + } + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp // see if its a known type int i = _ems_findType(type); if ((ems_getLogging() == EMS_SYS_LOGGING_BASIC) || (ems_getLogging() == EMS_SYS_LOGGING_VERBOSE)) { if (i == -1) { - myDebug("Requesting type (0x%02X) from dest 0x%02X\n", type, dest); + myDebug("Requesting type (0x%02X) from dest 0x%02X", type, dest); } else { - myDebug("Requesting type %s(0x%02X) from dest 0x%02X\n", EMS_Types[i].typeString, type, dest); + myDebug("Requesting type %s(0x%02X) from dest 0x%02X", EMS_Types[i].typeString, type, dest); } } EMS_TxTelegram.action = EMS_TX_TELEGRAM_READ; // read command @@ -1065,19 +1384,21 @@ void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh) { */ void ems_sendRawTelegram(char * telegram) { uint8_t count = 0; - char * p, value[10]; + char * p; + char value[10] = {0}; _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp // get first value, which should be the src if (p = strtok(telegram, " ,")) { // delimiter - strcpy(value, p); + strlcpy(value, p, sizeof(value)); EMS_TxTelegram.data[0] = (uint8_t)strtol(value, 0, 16); } // and interate until end while (p != 0) { if (p = strtok(NULL, " ,")) { - strcpy(value, p); + strlcpy(value, p, sizeof(value)); uint8_t val = (uint8_t)strtol(value, 0, 16); EMS_TxTelegram.data[++count] = val; if (count == 1) { @@ -1103,74 +1424,87 @@ void ems_sendRawTelegram(char * telegram) { * Set the temperature of the thermostat */ void ems_setThermostatTemp(float temperature) { - _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx - - EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = EMS_ID_THERMOSTAT; - - myDebug("Setting new thermostat temperature\n"); - - if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_RC20) { - EMS_TxTelegram.type = EMS_TYPE_RC20Set; - EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_temp; - EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value - - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; - EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; - EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; - EMS_TxTelegram.comparisonPostRead = - EMS_TYPE_RC20StatusMessage; // call a different type to refresh temperature value - - } else if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_RC30) { - EMS_TxTelegram.type = EMS_TYPE_RC30Set; - EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_temp; - EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value - - EMS_TxTelegram.type_validate = EMS_TxTelegram.type; - EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; - EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; - EMS_TxTelegram.comparisonPostRead = - EMS_TYPE_RC30StatusMessage; // call a different type to refresh temperature value - } else if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_EASY) { - myDebug("Setting new thermostat temperature on an Easy (not working yet!)\n"); + if (!EMS_Sys_Status.emsThermostatEnabled) { return; } + if (!EMS_Thermostat.write_supported) { + myDebug("Write not supported for this model Thermostat"); + return; + } + + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + + _EMS_MODEL_ID model_id = EMS_Thermostat.model_id; + uint8_t type = EMS_Thermostat.type_id; + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = type; + + myDebug("Setting new thermostat temperature"); + + if (model_id == EMS_MODEL_RC20) { + EMS_TxTelegram.type = EMS_TYPE_RC20Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_temp; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage; // call a different type to refresh temperature value + } else if (model_id == EMS_MODEL_RC30) { + EMS_TxTelegram.type = EMS_TYPE_RC30Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_temp; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage; // call a different type to refresh temperature value + } else if (model_id == EMS_MODEL_RC35) { + EMS_TxTelegram.type = EMS_TYPE_RC35Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage; // call a different type to refresh temperature value + } + + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value + EMS_TxTelegram.type_validate = EMS_TxTelegram.type; + EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset; + EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue; + EMS_TxTelegram.forceRefresh = false; // send to MQTT is done automatically in EMS_TYPE_RC30StatusMessage EMS_TxQueue.push(EMS_TxTelegram); } /** - * Set the thermostat working mode (0=low, 1=manual, 2=auto/clock) + * Set the thermostat working mode (0=low/night, 1=manual/day, 2=auto/clock) * 0xA8 on a RC20 and 0xA7 on RC30 */ void ems_setThermostatMode(uint8_t mode) { - if (EMS_ID_THERMOSTAT == EMS_ID_THERMOSTAT_EASY) { - // doesn't support Easy yet + if (!EMS_Sys_Status.emsThermostatEnabled) { return; } - myDebug("Setting thermostat mode to %d\n", mode); + if (!EMS_Thermostat.write_supported) { + myDebug("Write not supported for this model Thermostat"); + return; + } + + _EMS_MODEL_ID model_id = EMS_Thermostat.model_id; + uint8_t type = EMS_Thermostat.type_id; + + myDebug("Setting thermostat mode to %d", mode); _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = EMS_ID_THERMOSTAT; + EMS_TxTelegram.dest = type; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; EMS_TxTelegram.dataValue = mode; // handle different thermostat types - if (EMS_ID_THERMOSTAT == EMS_ID_THERMOSTAT_RC20) { + if (model_id == EMS_MODEL_RC20) { EMS_TxTelegram.type = EMS_TYPE_RC20Set; EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_mode; - } else if (EMS_ID_THERMOSTAT == EMS_ID_THERMOSTAT_RC30) { + } else if (model_id == EMS_MODEL_RC30) { EMS_TxTelegram.type = EMS_TYPE_RC30Set; EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_mode; - } else { - myDebug("Error! not supported\n"); - return; + } else if (model_id == EMS_MODEL_RC35) { + EMS_TxTelegram.type = EMS_TYPE_RC35Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_mode; } EMS_TxTelegram.type_validate = EMS_TxTelegram.type; // callback to EMS_TYPE_RC30Temperature to fetch temps @@ -1187,16 +1521,17 @@ void ems_setThermostatMode(uint8_t mode) { */ void ems_setWarmWaterTemp(uint8_t temperature) { // check for invalid temp values - if ((temperature < 30) || (temperature > 90)) { + if ((temperature < 30) || (temperature > EMS_BOILER_TAPWATER_TEMPERATURE_MAX)) { return; } - myDebug("Setting boiler warm water temperature to %d C\n", temperature); + myDebug("Setting boiler warm water temperature to %d C", temperature); _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = EMS_ID_BOILER; + EMS_TxTelegram.dest = EMS_Boiler.type_id; EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwtemp; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; @@ -1212,16 +1547,35 @@ void ems_setWarmWaterTemp(uint8_t temperature) { } /** - * Activate / De-activate the Warm Water 0x33 - * true = on, false = off + * Set the warm water mode to comfort ot Eco */ -void ems_setWarmWaterActivated(bool activated) { - myDebug("Setting boiler warm water %s\n", activated ? "on" : "off"); +void ems_setWarmWaterModeComfort(bool comfort) { + myDebug("Setting boiler warm water to comfort mode %s\n", comfort ? "Comfort" : "Eco"); _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = EMS_ID_BOILER; + EMS_TxTelegram.dest = EMS_Boiler.type_id; + EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; + EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwComfort; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + EMS_TxTelegram.dataValue = + (comfort ? EMS_VALUE_UBAParameterWW_wwComfort_Comfort : EMS_VALUE_UBAParameterWW_wwComfort_Eco); // 0x00 is on, 0xD8 is off + EMS_TxQueue.push(EMS_TxTelegram); +} + +/** + * Activate / De-activate the Warm Water 0x33 + * true = on, false = off + */ +void ems_setWarmWaterActivated(bool activated) { + myDebug("Setting boiler warm water %s", activated ? "on" : "off"); + + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = EMS_Boiler.type_id; EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwactivated; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; @@ -1236,17 +1590,18 @@ void ems_setWarmWaterActivated(bool activated) { * Using the type 0x1D to put the boiler into Test mode. This may be shown on the boiler with a flashing 'T' */ void ems_setWarmTapWaterActivated(bool activated) { - myDebug("Setting boiler warm tap water %s\n", activated ? "on" : "off"); + myDebug("Setting boiler warm tap water %s", activated ? "on" : "off"); _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp // clear Tx to make sure all data is set to 0x00 - for (int i = 0; (i < EMS_TX_MAXBUFFERSIZE); i++) { + for (int i = 0; (i < EMS_MAX_TELEGRAM_LENGTH); i++) { EMS_TxTelegram.data[i] = 0x00; } EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = EMS_ID_BOILER; + EMS_TxTelegram.dest = EMS_Boiler.type_id; EMS_TxTelegram.type = EMS_TYPE_UBAFunctionTest; EMS_TxTelegram.offset = 0; EMS_TxTelegram.length = 22; // 17 bytes of data including header and CRC @@ -1284,9 +1639,10 @@ void ems_setWarmTapWaterActivated(bool activated) { void ems_setExperimental(uint8_t value) { /* _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_TxTelegram.action = EMS_TX_TELEGRAM_READ; // read command - EMS_TxTelegram.dest = EMS_ID_THERMOSTAT; // set 8th bit to indicate a read + EMS_TxTelegram.dest = EMS_Thermostat.type; // set 8th bit to indicate a read EMS_TxTelegram.offset = 0; // 0 for all data EMS_TxTelegram.length = 8; EMS_TxTelegram.type = 0xF0; diff --git a/src/ems.h b/src/ems.h index bce7ae158..d6921d5d2 100644 --- a/src/ems.h +++ b/src/ems.h @@ -1,26 +1,28 @@ /* * Header file for EMS.cpp * - * You shouldn't need to change much in this file */ #pragma once +#include "emsuart.h" +#include "my_config.h" // include custom configuration settings #include +#include // https://github.com/rlogiacco/CircularBuffer +#include // EMS IDs -#define EMS_ID_NONE 0x00 // Fixed - used as a dest in broadcast messages and empty type IDs -#define EMS_ID_BOILER 0x08 // Fixed - also known as MC10. -#define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as "Service Key" +#define EMS_ID_NONE 0x00 // Fixed - used as a dest in broadcast messages and empty type IDs +#define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as "Service Key" -#define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC -#define EMS_MAX_TELEGRAM_LENGTH 99 // max length of a telegram, including CRC +//#define EMS_ID_THERMOSTAT 0xFF // Fixed - to recognize a Thermostat +//#define EMS_ID_BOILER 0x08 // Fixed - also known as MC10. -#define EMS_TX_MAXBUFFERSIZE 128 // max size of the buffer. packets are 32 bits +#define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC -#define EMS_ID_THERMOSTAT_RC20 0x17 // RC20 (e.g. Moduline 300) -#define EMS_ID_THERMOSTAT_RC30 0x10 // RC30 (e.g. Moduline 400) -#define EMS_ID_THERMOSTAT_EASY 0x18 // TC100 (Nefit Easy) +// max length of a telegram, including CRC, for Rx and Tx. +// This can differs per firmware version and typically 32 is the max +#define EMS_MAX_TELEGRAM_LENGTH 99 // define here the EMS telegram types you need @@ -41,8 +43,11 @@ #define EMS_TYPE_UBASetPoints 0x1A #define EMS_TYPE_UBAFunctionTest 0x1D -#define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature -#define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated +#define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature +#define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated +#define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode +#define EMS_VALUE_UBAParameterWW_wwComfort_Comfort 0x00 // the value for comfort +#define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco /* * Thermostat... @@ -64,14 +69,21 @@ #define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode #define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature +// RC35 specific - not implemented yet +#define EMS_TYPE_RC35StatusMessage 0x3E // is an automatic thermostat broadcast giving us temps +#define EMS_TYPE_RC35Set 0x3D // for setting values like temp and mode +#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode +#define EMS_OFFSET_RC35Set_temp 2 // position of thermostat setpoint temperature + // Easy specific #define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat // default values -#define EMS_VALUE_INT_ON 1 // boolean true -#define EMS_VALUE_INT_OFF 0 // boolean false -#define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit ints -#define EMS_VALUE_FLOAT_NOTSET -255 // float unset +#define EMS_VALUE_INT_ON 1 // boolean true +#define EMS_VALUE_INT_OFF 0 // boolean false +#define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit ints +#define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs +#define EMS_VALUE_FLOAT_NOTSET -255 // float unset /* EMS UART transfer status */ typedef enum { @@ -116,6 +128,7 @@ typedef struct { bool emsBoilerEnabled; // is the boiler online _EMS_SYS_LOGGING emsLogging; // logging bool emsRefreshed; // fresh data, needs to be pushed out to MQTT + bool emsBusConnected; // is there an active bus } _EMS_Sys_Status; // The Tx send package @@ -130,11 +143,13 @@ typedef struct { uint8_t comparisonValue; // value to compare against during a validate uint8_t comparisonOffset; // offset of where the byte is we want to compare too later uint8_t comparisonPostRead; // after a successful write call this to read - bool hasSent; // has been sent, just pending ack bool forceRefresh; // should we send to MQTT after a successful Tx? - uint8_t data[EMS_TX_MAXBUFFERSIZE]; + unsigned long timestamp; // when created + uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; } _EMS_TxTelegram; +#define EMS_TX_TELEGRAM_QUEUE_MAX 20 // max size of Tx FIFO queue + // default empty Tx const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { EMS_TX_TELEGRAM_INIT, // action @@ -147,11 +162,42 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { 0, // comparisonValue 0, // comparisonOffset EMS_ID_NONE, // comparisonPostRead - false, // hasSent false, // forceRefresh + 0, // timestamp {0x00} // data }; +// Known Buderus non-Thermostat types +typedef enum { + EMS_MODEL_NONE, + EMS_MODEL_ALL, // common for all devices + + // service key + EMS_MODEL_SERVICEKEY, // this is us + + // main buderus boiler type devices + EMS_MODEL_BK15, + EMS_MODEL_UBA, + EMS_MODEL_BC10, + EMS_MODEL_MM10, + EMS_MODEL_WM10, + + // thermostats + EMS_MODEL_ES73, + EMS_MODEL_RC20, + EMS_MODEL_RC30, + EMS_MODEL_RC35, + EMS_MODEL_EASY + +} _EMS_MODEL_ID; + +typedef struct { + _EMS_MODEL_ID model_id; + uint8_t product_id; + uint8_t type_id; + char model_string[50]; +} _Model_Type; + /* * Telegram package defintions */ @@ -160,54 +206,81 @@ typedef struct { // UBAParameterWW 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 // UBAMonitorFast - uint8_t selFlowTemp; // Selected flow temperature - float curFlowTemp; // Current flow temperature - float retTemp; // Return temperature - uint8_t burnGas; // Gas on/off - uint8_t fanWork; // Fan on/off - uint8_t ignWork; // Ignition on/off - uint8_t heatPmp; // Circulating pump on/off - uint8_t wWHeat; // 3-way valve on WW - uint8_t wWCirc; // Circulation on/off - uint8_t selBurnPow; // Burner max power - uint8_t curBurnPow; // Burner current power - float flameCurr; // Flame current in micro amps - float sysPress; // System pressure + uint8_t selFlowTemp; // Selected flow temperature + float curFlowTemp; // Current flow temperature + float retTemp; // Return temperature + uint8_t burnGas; // Gas on/off + uint8_t fanWork; // Fan on/off + uint8_t ignWork; // Ignition on/off + uint8_t heatPmp; // Circulating pump on/off + uint8_t wWHeat; // 3-way valve on WW + uint8_t wWCirc; // Circulation on/off + uint8_t selBurnPow; // Burner max power + uint8_t curBurnPow; // Burner current power + float flameCurr; // Flame current in micro amps + float sysPress; // System pressure + uint8_t serviceCodeChar1; // First Character in status/service code + uint8_t serviceCodeChar2; // Second Character in status/service code // UBAMonitorSlow float extTemp; // Outside temperature float boilTemp; // Boiler temperature uint8_t pumpMod; // Pump modulation - uint16_t burnStarts; // # burner restarts - uint16_t burnWorkMin; // Total burner operating time - uint16_t heatWorkMin; // Total heat operating time + uint32_t burnStarts; // # burner restarts + uint32_t burnWorkMin; // Total burner operating time + uint32_t heatWorkMin; // Total heat operating time // UBAMonitorWWMessage float 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 + + // UBATotalUptimeMessage + uint32_t UBAuptime; // Total UBA working hours // calculated values uint8_t tapwaterActive; // Hot tap water is on/off uint8_t heatingActive; // Central heating is on/off + // settings + char version[10]; + uint8_t type_id; + _EMS_MODEL_ID model_id; } _EMS_Boiler; +// Definition for thermostat type +typedef struct { + _EMS_MODEL_ID model_id; + bool read_supported; + bool write_supported; +} _Thermostat_Type; + +#define EMS_THERMOSTAT_READ_YES true +#define EMS_THERMOSTAT_READ_NO false +#define EMS_THERMOSTAT_WRITE_YES true +#define EMS_THERMOSTAT_WRITE_NO false + // Thermostat data typedef struct { - uint8_t type; // thermostat type (RC30, Easy etc) - float setpoint_roomTemp; // current set temp - float curr_roomTemp; // current room temp - uint8_t mode; // 0=low, 1=manual, 2=auto - uint8_t hour; - uint8_t minute; - uint8_t second; - uint8_t day; - uint8_t month; - uint8_t year; + uint8_t type_id; // the type ID of the thermostat + _EMS_MODEL_ID model_id; // which Thermostat type + bool read_supported; + bool write_supported; + char version[10]; + float setpoint_roomTemp; // current set temp + float curr_roomTemp; // current room temp + uint8_t mode; // 0=low, 1=manual, 2=auto + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t day; + uint8_t month; + uint8_t year; } _EMS_Thermostat; // call back function signature @@ -215,17 +288,11 @@ typedef void (*EMS_processType_cb)(uint8_t * data, uint8_t length); // Definition for each EMS type, including the relative callback function typedef struct { - uint8_t src; + _EMS_MODEL_ID model_id; uint8_t type; const char typeString[50]; EMS_processType_cb processType_cb; -} _EMS_Types; - -// Definition for thermostat type -typedef struct { - uint8_t id; - const char typeString[50]; -} _Thermostat_Types; +} _EMS_Type; // ANSI Colors #define COLOR_RESET "\x1B[0m" @@ -255,28 +322,37 @@ void ems_setExperimental(uint8_t value); void ems_setPoll(bool b); void ems_setTxEnabled(bool b); void ems_setThermostatEnabled(bool b); +void ems_setBoilerEnabled(bool b); void ems_setLogging(_EMS_SYS_LOGGING loglevel); void ems_setEmsRefreshed(bool b); +void ems_setBusConnected(bool b); +void ems_setWarmWaterModeComfort(bool comfort); void ems_getThermostatValues(); +void ems_getBoilerValues(); bool ems_getPoll(); bool ems_getTxEnabled(); bool ems_getThermostatEnabled(); bool ems_getBoilerEnabled(); +bool ems_getBusConnected(); _EMS_SYS_LOGGING ems_getLogging(); uint8_t ems_getEmsTypesCount(); -uint8_t ems_getThermostatTypesCount(); bool ems_getEmsRefreshed(); +void ems_getVersions(); +_EMS_MODEL_ID ems_getThermostatModel(); -void ems_printAllTypes(); -void ems_printThermostatType(); -void ems_printTxQueue(); +void ems_printAllTypes(); +char * ems_getThermostatType(char * buffer); +void ems_printTxQueue(); +char * ems_getBoilerType(char * buffer); // private functions uint8_t _crcCalculator(uint8_t * data, uint8_t len); void _processType(uint8_t * telegram, uint8_t length); void _debugPrintPackage(const char * prefix, uint8_t * data, uint8_t len, const char * color); void _ems_clearTxData(); +int _ems_findModel(_EMS_MODEL_ID model_id); +char * _ems_buildModelString(char * buffer, uint8_t size, _EMS_MODEL_ID model_id); // global so can referenced in other classes extern _EMS_Sys_Status EMS_Sys_Status; diff --git a/src/my_config.h b/src/my_config.h index d2a1a4840..84fd333ec 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -9,31 +9,25 @@ // these are set as -D build flags during compilation // they can be set in platformio.ini or alternatively hard coded here +/* +#define WIFI_SSID "" +#define WIFI_PASSWORD "" +#define MQTT_IP "" +#define MQTT_USER "" +#define MQTT_PASS "" +*/ -// WIFI settings -//#define WIFI_SSID "" -//#define WIFI_PASSWORD "" - -// MQTT settings -// Note port is the default 1883 -//#define MQTT_IP "" -//#define MQTT_USER "" -//#define MQTT_PASS "" - -// default values -#define BOILER_THERMOSTAT_ENABLED 1 // thermostat support is enabled (1) -#define BOILER_SHOWER_TIMER 1 // monitors how long a shower has taken -#define BOILER_SHOWER_ALERT 0 // send alert if showetime exceeded - -// define here the Thermostat type. see ems.h for the supported types -#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_RC20 -//#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_RC30 -//#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_EASY +// default values for shower logic on/off +#define BOILER_SHOWER_TIMER 1 // monitors how long a shower has taken +#define BOILER_SHOWER_ALERT 0 // send alert if showetime exceeded // trigger settings to determine if hot tap water or the heating is active #define EMS_BOILER_BURNPOWER_TAPWATER 100 #define EMS_BOILER_SELFLOWTEMP_HEATING 70 +//define maximum settable tapwater temperature, not every installation supports 90 degrees +#define EMS_BOILER_TAPWATER_TEMPERATURE_MAX 60 + // if using the shower timer, change these settings #define SHOWER_PAUSE_TIME 15000 // in ms. 15 seconds, max time if water is switched off & on during a shower #define SHOWER_MIN_DURATION 120000 // in ms. 2 minutes, before recognizing its a shower @@ -41,8 +35,10 @@ #define SHOWER_OFFSET_TIME 5000 // in ms. 5 seconds grace time, to calibrate actual time under the shower #define SHOWER_COLDSHOT_DURATION 10 // in seconds. 10 seconds for cold water before turning back hot water -// if using LEDs to show traffic, configure the GPIOs here -// only works if -DUSE_LED is set in platformio.ini -#define LED_RX D1 // GPIO5 -#define LED_TX D2 // GPIO4 -#define LED_ERR D3 // GPIO0 +// The LED for showing connection errors, either onboard or via an external pull-up LED +// can be disabled using -DNO_LED build flag in platformio.ini +#define BOILER_LED LED_BUILTIN // LED_BULLETIN for onboard LED +//#define BOILER_LED D1 // for external LED like on the latest bbqkees boards + +// set this if using an external temperature sensor like a DS18B20 +#define TEMPERATURE_SENSOR_PIN D7 diff --git a/src/version.h b/src/version.h index c65eb0bbe..fa3154d9f 100644 --- a/src/version.h +++ b/src/version.h @@ -1,2 +1,5 @@ +#pragma once + #define APP_NAME "EMS-ESP-Boiler" -#define APP_VERSION "1.1.0" +#define APP_VERSION "1.2.0" +#define APP_HOSTNAME "boiler"