diff --git a/CHANGELOG.md b/CHANGELOG.md index a35d4d7f3..f059cb5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,280 @@ -# EMS-ESP-Boiler Changelog +# EMS-ESP Changelog 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.5.6] 2019-03-09 ### Added -- Setting the mode and setpoint temperature on a RC35 +- test_mode option + +### Changed + +- upgraded MyESP library +- minor changes + + +## [1.5.5] 2019-03-07 + +### Fixed +- Support the latest ArduinoJson v6 and espressif8266 2.0.4 libraries (in PlatformIO do a `pio lib update` and `pio update`) + +### Changed + +- MQTT keep alive to 2 minutes (60 seconds was just too short for slower networks) +- Improved MQTT startup time +- Setting wifi or mqtt settings are immediate, no need to restart the ESP +- Text changes in the help + +### Added +- Show if MQTT is connected +- Show version of MyESP (the custom MQTT, Wifi, OTA, MDNS, Telnet library) +- EMS-OT OpenTherm connector + +## [1.5.4] 2019-03-03 + +### Changed + +- MQTT keep alive changed from 5 minutes to 1 minute + +### Added +- Callback for OTA. This is used to disable EMS bus during a firmware OTA update, which caused problems with the latest ESP89266 core libraries +- Added rough estimate of WiFi signal strength to info page +- Added the build time & date to the info page (optional in platformio.ini) + +## [1.5.3] 2019-02-22 + +### Changed + +- Support for latest esp8266 arduino core version [2.5.0](https://github.com/esp8266/Arduino/releases/tag/2.5.0) and platform espressif8266 version 2.0.0 +- Added board type to the info screen + +### Added + +- Improved MQTT LWT (Last Will Testament). Uses payload called 'online' and 'offline'. See https://github.com/proddy/EMS-ESP/issues/57 +- Added ESP32 support to MyESP library +- Added Bosch Easy thermostat, Buderus Logamax U122 +- Support for changing boiler wwtemp via MQTT (merge #58 from egrekov). thanks! + +### Removed + +- Custom MDNS support. Now handled much better in the new esp core under OTA + +## [1.5.2] 2019-02-04 + +### Changed + +- Change wifi settings using the `set wifi ` command + +### Added + +- Added incoming MQTT "TOPIC_BOILER_WWACTIVATED" to set the warm water on/off. Payload is 1 or 0. See [issue](https://github.com/proddy/EMS-ESP/issues/46#issuecomment-460375689). +- Added the list of all MQTT topics to the README file + +## [1.5.1] 2019-02-03 + +### Fixed + +- issue with Serial monitoring conflicting with UART when both running +- Fixed typo with -D settings in the example platformio.ini + +### Changed + +- `thermostat temp` now except floats (e.g. 20.5). Some thermostats may round up or down if they use 0.5 intervals. + +## [1.5.0] 2019-02-03 + +### Added + +- Support for RC10 thermostat +- New command `set serial` + +### Changed + +- Improved Tx logic. Retries are more efficient and startup is faster and less error prone. +- "# Rx telegrams" and "# Tx telegrams" show number of successful Reads and Writes initiated by the user or automatically by the code. This makes it easy to see if the Tx is working. +- Some refactoring in preparation for the EMS+ support coming soon + +### Removed + +- Removed the `poll` and `tx` commands +- `DEBUG_SUPPORT`. Now controlled with the 'set serial' command +- removed MQTT and WIFI settings from my_config.h. These have to be set either within the application (using set) or hardcoded in platformio.ini You can now check in `my_config.h` without everyone seeing your passwords! +- TxCapable removed from `info` + +## [1.4.1] 2019-01-29 + +### Added + +- The led pin, dallas pin and both thermostat and boiler type IDs can be set with the application, and stored. + +### Changed + +- some minor improvements to autodetect + +## [1.4.0] 2019-01-27 + +### Changed + +- last will MQTT topic prefixed with a header like the rest of the topics +- All double and float numbers rendered to 2 decimal places (precision = 2) +- Default logging set to None when starting a telnet session + +### Added + +- Added support for external Dallas sensors (DS1822, DS18S20, DS18B20, DS1825). See readme +- Added UBAParametersMessage type to fetch boiler modulation min & max values +- Report shows system load average + +## [1.3.2] 2019-01-23 + +### Fixed + +- Handle thermostats that don't have builtin temperature sensors when showing current temperature (https://github.com/proddy/EMS-ESP/issues/18#issuecomment-451012963) + +### Changed + +- Improved way to identify if the EMS bus is connected +- Improved 'types' command to show more details +- Improved auto detect of thermostat types + +### Added + +- Some more devices like the Nefit Topline & RC310 thermostat recognition +- Added a check to see Tx is possible. See 'Tx Capable' under the 'info' screen + +### Removed + +- Removed `MY_BOILER_MODELID` from `my_config.h`. It's always hardcoded. + +## [1.3.1] 2019-01-12 + +### Fixed + +- telnet commands with set are no longer forced to lower case + +### Changed + +- Custom settings (e.g set led) moved outside MyESP +- Moved all MQTT to my_config.h making it independent from Home Assistant + +### Added + +- MQTT keep alive, last will testament and other settings all configurable in my_config.h +- RC35: MQTT day/night/auto mode; sets setpoint temperature in type 0x3D depends on current night/day Mode (@SpaceTeddy) [#33](https://github.com/proddy/EMS-ESP/pull/33) + +## [1.3.0] 2019-01-09 + +### Changed + +- Renamed project from EMS-ESP-Boiler to EMS-ESP since it's kinda EMS generic now +- Support for RC20F and RFM20 (https://github.com/proddy/EMS-ESP/issues/18) +- Moved all EMS device information into a separate file `ems_devices.h` so no longer need to touch `ems.h` +- Telnet commands can be strings now and output is suspended when typing + +### Removed + +- Removed SHOWER_TEST +- Removed WIFI and MQTT credentials from the platformio.ini file + +### Added + +- Settings are saved and loaded from the ESP8266's file system (SPIFFS). Can be set using the 'set' command +- Improved support when in Access Point mode (192.168.4.1) +- pre-built firmwares are back + +## [1.2.4] 2019-01-04 + +### Changed + +- Scanning known EMS Devices now ignores duplicates (https://github.com/proddy/EMS-ESP/pull/30) +- ServiceCode stored as a two byte char +- Support for RC20F and RFM20 (https://github.com/proddy/EMS-ESP/issues/18) + +## [1.2.3] 2019-01-03 + +### Fixed + +- Can now hardcode Boiler and Thermostat types in my_config.h to bypass auto-detection +- Fixed MQTT subscribing to Heating and Hot Water active topics +- Fixed for listening to incoming MQTT topics (https://github.com/proddy/EMS-ESP/issues/27) +- Fixed handling of current temperature on an RC35-type thermostat that doesn't have a sensor (https://github.com/proddy/EMS-ESP/issues/18) + +## [1.2.2] 2019-01-02 + +### Fixed + +- Issues in 1.2.1 (see https://github.com/proddy/EMS-ESP/issues/25) +- Logic for determining if there is activity on the EMS bus and using the onboard LEDs properly + +## [1.2.1] 2019-01-02 + +### Fixed + +- Only process broadcast messages if the offset (byte 4) is 0. (https://github.com/proddy/EMS-ESP/issues/23) +- Improved checking for duplicate sent Tx telegrams by comparing CRCs +- Removed distiquishing between noise on the line and corrupt telegrams (https://github.com/proddy/EMS-ESP/issues/24) + +## [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 + +- 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/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 + +### Removed + +- Espurna build notes and ready made firmware ## [1.1.0] 2018-12-22 ### 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) +- [Fixed condition where all telegram types were processed, instead of only broadcasts or our own reads](https://github.com/proddy/EMS-ESP/issues/15) ### Added - Created this CHANGELOG.md file! -- [Added support for the Nefit Easy thermostat, reading of temperature values only](https://github.com/proddy/EMS-ESP-Boiler/issues/9) - note *read only* (big thanks @**kroon040** for lending me an Easy device) -- [Added support for RC35/Moduline 400](https://github.com/proddy/EMS-ESP-Boiler/issues/14) - *read only* -- [New raw logging mode for logging](https://github.com/proddy/EMS-ESP-Boiler/issues/11) -- [New 'r'command to send raw data to EMS](https://github.com/proddy/EMS-ESP-Boiler/issues/11) -- [Added MQTT messages for hot water on and heating on](https://github.com/proddy/EMS-ESP-Boiler/issues/10) +- [Added support for the Nefit Easy thermostat, reading of temperature values only](https://github.com/proddy/EMS-ESP/issues/9) - note *read only* (big thanks @**kroon040** for lending me an Easy device) +- [Added support for RC35/Moduline 400](https://github.com/proddy/EMS-ESP/issues/14) - *read only* +- [New raw logging mode for logging](https://github.com/proddy/EMS-ESP/issues/11) +- [New 'r'command to send raw data to EMS](https://github.com/proddy/EMS-ESP/issues/11) +- [Added MQTT messages for hot water on and heating on](https://github.com/proddy/EMS-ESP/issues/10) - Implemented FIFO circular buffer queue for up to 20 Tx messages (Q command to show queue) - Toggle Tx transmission via telnet (use 'X' command) - Show thermostat type in help stats (use 's' command) diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 1129666ae..000000000 --- a/LICENSE.md +++ /dev/null @@ -1,13 +0,0 @@ -#### Copyright 2018 [Paul Derbsyhire](mailto:dev@derbyshire.nl). All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met : - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and / or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT SHALL OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are those of the -authors and should not be interpreted as representing official policies, either expressed -or implied, of [Paul Derbyshire](mailto:dev@derbyshire.nl). \ No newline at end of file diff --git a/README.md b/README.md index df9114d5a..f9aad9cf8 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,38 @@ -# EMS-ESP-Boiler +# EMS-ESP -EMS-ESP-Boiler is a project to build a controller circuit running with an ESP8266 to communicate with EMS (Energy Management System) based Boilers and Thermostats from the Bosch range and compatibles such as Buderus, Nefit, Junkers etc. +EMS-ESP is a project to build an electronic controller circuit using an Espressif ESP8266 microcontroller to communicate with EMS (Energy Management System) based Boilers and Thermostats from the Bosch range and compatibles such as Buderus, Nefit, Junkers etc. -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. +There are 3 parts to this project, first the design of the circuit, secondly the code for the ESP8266 microcontroller firmware with telnet and MQTT support, and lastly an example configuration for Home Assistant to monitor the data and issue direct commands via a MQTT broker. -[![version](https://img.shields.io/badge/version-1.1.0-brightgreen.svg)](CHANGELOG.md) -[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.org/xoseperez/espurna/tree/dev/) -[![license](https://img.shields.io/github/license/xoseperez/espurna.svg)](LICENSE) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/b8880625bdf841d4adb2829732030887)](https://app.codacy.com/app/proddy/EMS-ESP?utm_source=github.com&utm_medium=referral&utm_content=proddy/EMS-ESP&utm_campaign=Badge_Grade_Settings) +[![version](https://img.shields.io/badge/version-1.5.5-brightgreen.svg)](CHANGELOG.md) -- [EMS-ESP-Boiler](#ems-esp-boiler) +- [EMS-ESP](#ems-esp) - [Introduction](#introduction) - - [Supported Boilers Types](#supported-boilers-types) + - [Supported EMS Devices](#supported-ems-devices) - [Supported ESP8266 devices](#supported-esp8266-devices) - [Getting Started](#getting-started) - [Monitoring The Output](#monitoring-the-output) - [Building The Circuit](#building-the-circuit) - [Powering The EMS Circuit](#powering-the-ems-circuit) + - [Adding external temperature sensors](#adding-external-temperature-sensors) - [How The EMS Bus Works](#how-the-ems-bus-works) - [EMS IDs](#ems-ids) - [EMS Polling](#ems-polling) - [EMS Broadcasting](#ems-broadcasting) - [EMS Reading and Writing](#ems-reading-and-writing) - [The ESP8266 Source Code](#the-esp8266-source-code) - - [Supported EMS Types](#supported-ems-types) - - [Supported Thermostats](#supported-thermostats) - - [RC20 (Moduline 300)](#rc20-moduline-300) - - [RC30 (Moduline 400)](#rc30-moduline-400) - - [RC35](#rc35) - - [TC100/TC200 (Nefit Easy)](#tc100tc200-nefit-easy) + - [Special EMS Types](#special-ems-types) + - [Which thermostats are supported?](#which-thermostats-are-supported) - [Customizing The Code](#customizing-the-code) - [Using MQTT](#using-mqtt) - [The Basic Shower Logic](#the-basic-shower-logic) - [Home Assistant Configuration](#home-assistant-configuration) - [Building The Firmware](#building-the-firmware) - [Using PlatformIO Standalone](#using-platformio-standalone) - - [Using ESPurna](#using-espurna) - - [Using Pre-built Firmware](#using-pre-built-firmware) - [Building Using Arduino IDE](#building-using-arduino-ide) + - [Using the Pre-built Firmware](#using-the-pre-built-firmware) + - [Troubleshooting](#troubleshooting) - [Known Issues](#known-issues) - [Wish List](#wish-list) - [Your Comments and Feedback](#your-comments-and-feedback) @@ -44,82 +40,84 @@ There are 3 parts to this project, first the design of the circuit, second the c ## Introduction -My original intention for this home project with to build my own smart thermostat for my Nefit Trendline boiler and then have it controlled automatically via [Home Assistant](https://www.home-assistant.io/) on my mobile phone. I had a few ESP32s and ESP8266s lying around from previous IoT projects and building a specific circuit to decode the EMS messages was a nice challenge into designing complete end-to-end complex electronic circuits. I then began adding new features such as timing how long the shower would be running for and subsequently triggering an alarm (actually a shot of cold water) after a certain period. +The original intention for this home project was to build a custom smart thermostat that interfaces with my Nefit Trendline HRC30 boiler and have it controlled via a mobile app using MQTT. I had a few cheap ESP32s and ESP8266s microcontrollers lying around from previous IoT projects and learning how to build a circuit to decode the EMS bus messages seemed like a nice challenge. -Acknowledgments and kudos to the following people and their open-sourced projects that have helped me get this far: +Acknowledgments and kudos to the following people who have open-sourced their projects: - **susisstrolch** - Probably the first working version of the EMS bridge circuit I found designed for the ESP8266. I borrowed Juergen's [schematic](https://github.com/susisstrolch/EMS-ESP12) and parts of his code logic. + **susisstrolch** - One of the first working versions of the EMS bridge circuit I found designed for specifically for the ESP8266. I borrowed Juergen's [schematic](https://github.com/susisstrolch/EMS-ESP12) and parts of his code ideas for reading telegrams. - **bbqkees** - Kees built a [circuit](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz) and some sample Arduino code to read from the EMS and push messages to Domoticz. His SMD board is also now available for purchase. + **bbqkees** - Kees built a working [circuit](https://shop.hotgoodies.nl/ems/) and his SMD board is available for purchase on his website. - **EMS Wiki** - A comprehensive [reference](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme) for decoding the EMS telegrams, which I found not always to be 100% accurate. It's in German so use Google Translate if you need help. + **EMS Wiki** - A comprehensive [reference](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme) (in German) for the EMS bus which is a little outdated, not always 100% accurate and sadly no longer maintained. -## Supported Boilers Types +## Supported EMS Devices -Most Bosch branded boilers that support the Logamatic EMS (and EMS+) bus protocols work with this design. Which are Nefit, Buderus, Worcester and Junkers and copyrighted. Please make sure you read the **Disclaimer** carefully before sending ambigious messages to your EMS bus as you cause device damage. +Most Bosch branded boilers that support the Logamatic EMS bus protocols work with this design. This includes Nefit, Buderus, Worcester and Junkers (all copyrighted). Please make sure you read the **Disclaimer** carefully before sending ambiguous messages to your EMS bus as you could cause serious damage to your equipment. + +Note support for the later EMS Plus (EMS+ or EMS2) standard hasn't been added yet to the library. If you'd like to help please reach out. ## Supported ESP8266 devices -I've tested the code and circuit with a few ESP8266 development boards such as the Wemos D1 Mini, Wemos D1 Mini Pro, Nodemcu0.9 and Nodemcu2 boards. It will also work on bare ESP8266 chips such as the E-12s but do make sure you disabled the LED support and wire the UART correctly as the code doesn't use the normal Rx and Tx pins. This is explained below. +The code and circuit has been tested with a few ESP8266 development boards such as the Wemos D1 Mini, Wemos D1 Mini Pro, Nodemcu0.9 and Nodemcu2 dev boards. It will also work on bare ESP8266 chips such as the ESP-12E but do make sure you disabled the LED support and wire the UART correctly as the code doesn't use the normal Rx and Tx pins. ## Getting Started -1. Either build the circuit below or purchase a ready built board from bbqkees via his [GitHub](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz) page or the [Domoticz forum](http://www.domoticz.com/forum/viewtopic.php?f=22&t=22079&start=20). -2. Get an ESP8266 dev board and connect the 2 EMS output lines from the boiler to the circuit and the Rx and Tx out to ESP pins D7 and D8 respectively. The EMS connection can either be the 12-15V AC direct from the thermostat bus line or from the 3.5" Service Jack at the front. -3. Optionally connect the three LEDs to show Rx and Tx traffic and Error codes to pins D1, D2, D3 respectively. I use 220 Ohm pull-down resistors. These pins are configurable in ``boiler.ino``. This is further explained in the **code** section below. -4. Build and upload the firmware to the ESP8266 device. I used Platformio with Visual Studio. Do make sure you set the MQTT and WiFi credentials correctly and if you're not using MQTT leave the MQTT_IP blank. The firmware supports OTA too with the default hostname as 'boiler' (or 'boiler.' depending on your OS and how the mdns resolves hostnames). -5. Power the ESP either via USB or direct into the 5v vin pin from an external power 5V volts supply with min 400mA. -6. Attach the 3v3 out on the ESP8266 to the DC power line on the EMS circuit as indicated in the schematics. -7. The WiFi connects via DHCP by default. Find the IP by from your router and then telnet (port 23) to it. Tip: to enable Telnet on Windows run `dism /online /Enable-Feature /FeatureName:TelnetClient` or install something like [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). If everything is working you should see the messages appear in the window as shown in the next section. However if you're unable to locate the IP of the ESP then probably the WiFi failed to instantiate. In this case add -DUSE_SERIAL to the build options, connect at USB, build, upload and then use a terminal to connect to the serial port to see the debug messages. A word of warning, do not use both a USB and power from the EMS at the same time. +1. Either build the circuit described below or purchase a ready built board from bbqkees. +2. Grab any ESP8266 dev board. The latest bbqkees boards have a Wemos D1 pre-mounted with a copy of this firmware. +3. Optionally add external Dallas temperature sensors and an external LED. The default pins for these are D1 and D5 respectively. +4. Decide whether to compile and upload the code yourself using PlatformIO or just upload the pre-baked firmware using the esptool (read these [instructions](#using-the-pre-built-firmware)). If you want to build yourself now is the time to customize your settings in `my_custom.h`. Upload the firmware. +5. Connect a USB 5v power supply to the ESP8266 board, either via laptop/PC or external power supply. +7. When the ESP8266 starts up for the first time the onboard LED will be flashing. This is because the EMS bus is not yet connected. +8. If you haven't hardcoded the WiFi credentials in step 4, the ESP8266 will boot up in a WiFi Access Point (AP) mode with the ssid name `ems-esp`. Now you can either use a laptop and connect to this AP using Telnet to `192.168.1.4` or if its powered from a computers USB use a Serial monitor tool to the ESP's COM port. Tip: to enable Telnet on Windows 10 run `dism /online /Enable-Feature /FeatureName:TelnetClient` or install something like [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). +9. Next is to change some of the settings. Type `set` to list the current stored settings. Use `set wifi` to add your wifi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the device. +10. The `led_gpio` will default to the onboard LED (which is probably blinking now). Ignore `thermostat_type` and `boiler_type` as these will be auto-detected hopefully later on. +11. **Important**: If `serial` is set to `on` set it to `off` using `set serial off`. The EMS bus is disabled when the serial is on. This mode is only used for setting up a new board or debugging startup issues. +12. Hook up the ESP to the EMS board as follows: + +| EMS board | ESP8266 dev board | +| ----------|------------------ | +| Ground/G/J2| GND/G | +| Rx/J2 | D7 | +| Tx/J2 | D8 | +| VC/J2 | 3v3 or 5v | +13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5" service jack if you have an bbqkees board. +14. Reboot the ESP, either by the reset switch or pulling the power. +15. The ESP will first perform an autodetect to try and discover the EMS devices attached. If your boiler and thermostat are recognized it will set these types and store them for ever and ever. You can trace the output by telnet'ing to the board `telnet ems-esp.local`. Also type `info` to check what happened. +16. If your boiler/thermostat is not discovered create a GitHub issue stating the type and product ID. These will be added to the file `ems_devices.h` in a future release. +17. If all is well and there is traffic on the EMS bus the onboard LED will stop blinking and be permanently on. If this is annoying you can disable with `set led off`. To see the EMS messages type `set log v` for verbose logging. +18. And all is not well, check the wiring, make sure serial is off and look at the telnet session for errors. If in doubt, wipe the ESP with `pio run -t erase` and start again with step #3 ## Monitoring The Output Use the telnet client to inform you of all activity and errors real-time. This is an example of the telnet output: -![Telnet](doc/telnet/telnet_example.jpg) +![Telnet](doc/telnet/telnet_menu.jpg) -If you type 'l 4' and Enter, it will toggle verbose logging showing you more detailed messages. I use ANSI colors with white text for info messages, green for well formatted telegram packages (which have validated CRC checks), red for corrupt packages and yellow for send responses. +Type 'log v' and Enter and you'll be seeing verbose logging messages. ANSI colors with white text for info messages, green are for broadcast telegrams, yellow are the ones sent to us and red are for unknown data or telegrans which have failed the CRC check. ![Telnet](doc/telnet/telnet_verbose.PNG) -To see the current values of the Boiler and its parameters type 's' and hit Enter. Watch out for unsuccessful telegram packets in the #CrcErrors line. +To see the current stats and collected values type 'info'. Watch out for unsuccessful telegram packets in the #CrcErrors line. ![Telnet](doc/telnet/telnet_stats.PNG) -Commands can be issued directly to the EMS bus typing in a letter followed by an optional parameter and pressing Enter. Supported commands are: - -- **b** to send a read command to the boiler. The 2nd parameter is the type. For example 'b 33' will request type UBAParameterWW and bring back the Warm Water temperatures from the Boiler. -- **t** is similar, but to send a read command to the thermostat. -- **T** set the thermostat temperature to the given celsius value -- **w** to adjust the temperature of the warm water from the boiler -- **a** to turn the warm tap water on and off -- **h** to list all the recognized EMS types -- **P** to toggle the Polling response on/off (note it's not necessary to have Polling enabled to work) -- **m** to set the thermostat mode to manual or auto -- **S** to toggle the Shower Timer functionality on/off -- **A** to toggle the Shower Timer Alert functionality on/off - -**Disclaimer: be careful when sending values to the boiler. If in doubt you can always reset the boiler to its original factory settings by following the instructions in the user guide. On my **Nefit Trendline HRC30** that is done by holding down the Home and Menu buttons simultaneously for a few seconds, selecting factory settings from the scroll menu and lastly pressing the Reset button.** +**Disclaimer: be careful when sending values to the boiler. If in doubt you can always reset the boiler to its original factory settings by following the instructions in the user guide. For example on my Nefit Trendline that is done by holding down the Home and Menu buttons simultaneously for a few seconds, selecting factory settings from the scroll menu followed by pressing the Reset button.** ## Building The Circuit -The EMS circuit is really all credit to the hard work many people have done before me, noticeably *susisstrolch* with his ESP8266 [version](https://github.com/susisstrolch/EMS-ESP8266_12-PCB/tree/newmaster/Schematics/EMS-ESP8266-12). - -I've included a prototype boards you can build yourself on a breadboard. One part for only reading values from the Boiler and an extension with the write logic so you can also send commands. - -We need the Rx/Tx of the ESP8266 for flashing, so the code in ``emsuart.cpp`` switches the UART pins to use RX1 and TX1 (GPIO13/D7 and GPIO15/D8 respectively). This also prevents any bogus stack data being sent to EMS bus when the ESP8266 decides to crash like after a Watch Dog Reset. +Included is a prototype boards you can build yourself on a breadboard. The breadboard layout was done using [DIY Layout Creator](https://github.com/bancika/diy-layout-creator) and sources files are included in this repo. ![Breadboard Circuit](doc/schematics/breadboard.png) -The schematic used (as designed by [susisstrolch](https://github.com/susisstrolch/EMS-ESP8266_12-PCB)): +The schematic used: ![Schematic](doc/schematics/circuit.png) *Optionally I've also added 2 0.5A/72V polyfuses between the EMS and the two inductors L1 and L2 for extra protection.* -And lastly if you don't fancy building the circuit, [bbqkees](http://www.domoticz.com/forum/memberlist.php?mode=viewprofile&u=1736) can sell you one complete with SMD components which looks like the photo below when connected to a Wemos D1 Mini: +And here's a version using an early prototype board from **bbqkees**: ![WemosD1](doc/schematics/wemos_kees.png) @@ -134,33 +132,35 @@ The EMS circuit will work with both 3.3V and 5V. It's easiest though to power di | With Power Circuit | | ------------------------------------------ | -| ![Power circuit](doc/schematics/Schematic_EMS-ESP-Boiler-supercap.png) | +| ![Power circuit](doc/schematics/Schematic_EMS-ESP-supercap.png) | + +## Adding external temperature sensors + +The code supports auto-detection of Dallas type temperature sensors. The default gpio pin used on the ESP8266 is D5 but this can be configured in the setting menu (`set dallas_gpio`). The dallas chips DS1822, DS18S20, DS18B20, DS1825 are supported including their parasite varieties. ## How The EMS Bus Works -Packages are sent to the EMS "bus" from the Boiler and any other compatible connected devices via serial TTL transmission. The protocol is 9600 baud, 8N1 (8 bytes, no parity, 1 stop bit). Each package is terminated with a break signal ``, a 11-bit long low signal of zeros. +Packages are streamed to the EMS "bus" from any other compatible connected device via serial TTL transmission using protocol 9600 baud, 8N1 (8 bytes, no parity, 1 stop bit). Each package is terminated with a break signal ``, a 11-bit long low signal of zeros. A package can be a single byte (see Polling below) or a string of 6 or more bytes making up an actual data telegram. A telegram is always in the format: ``[src] [dest] [type] [offset] [data] [crc] `` -I reference the first 4 bytes as the *header* in this document. +The first 4 bytes is referenced as the *header* in this document. ### EMS IDs Each device has a unique ID. -The Boiler has an ID of 0x08 (type MC10) and also referred to as the Bus Master or UBA. +In this example a UBA boiler has an ID of 0x08 (such as a MC10) and also referred to as the Bus Master. -My thermostat, which is a* Moduline 300* uses the RC30 protocol and has an ID 0x17. If you're using a RC35 type thermostat such as the newer Moduline 300s or 400s use 0x10 and make adjustments in the code as appropriate. bbqkees did a nice write-up on his github page [here](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz/blob/master/README.md). - -Our circuit acts as a service key and thus uses an ID 0x0B. This ID is reserved for special devices intended for installation engineers for maintenance work. +The circuit acts as a service key and thus uses an ID 0x0B. This ID is reserved for special devices intended for service engineers. ### EMS Polling The bus master (boiler) sends out a poll request every second by sending out a sequential list of all possible IDs as a single byte followed by the break signal. The ID always has its high 8th bit (MSB) set so in the code we're looking for 1 byte messages matching the format `[dest|0x80] `. -Any connected device can respond to a Polling call with an acknowledging by sending back a single byte with its own ID. In our case we would listen for a `[0x8B] ` (meaning us) and then send back `[0x0B] ` to say we're alive and ready. Although I found this is not needed for normal operation so it's disabled as default in the code. +Any connected device can respond to a Polling request with an acknowledgement by sending back a single byte with its own ID. In our case we would listen for a `[0x8B] ` (meaning us) and then send back `[0x0B] ` to say we're alive and ready. Polling is also the trigger to start transmitting any packages queued for sending. It must be done within 200ms or the bus master will time out. @@ -168,7 +168,7 @@ Polling is also the trigger to start transmitting any packages queued for sendin When a device is broadcasting to everyone there is no specific destination needed. `[dest]` is always 0x00. -The tables below shows which types are broadcasted regularly by the boiler (ID 0x08) and thermostat (ID 0x17). The **data length** is excluding the 4 byte header and CRC and the **Name** references those in the [ems wiki](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme). +The tables below shows which types are broadcasted regularly by the boiler (in this case ID 0x08) and thermostat (ID 0x17). The **data length** is excluding the 4 byte header and CRC and the **Name** references those in the German [ems wiki](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme). | Source (ID) | Type ID | Name | Description | Data length | Frequency | | ------------- | ------- | ------------------- | -------------------------------------- | ----------- | ---------- | @@ -189,106 +189,86 @@ Refer to the code in `ems.cpp` for further explanation on how to parse these mes ### EMS Reading and Writing -Telegram packets can only be sent after the Boiler sends a poll to the sending device. The response can be a read command to request data or a write command to send data. At the end of the transmission a poll response is sent from the client (` `) to say we're all done and free up the bus for other clients. +Telegrams can only be sent after the Master (boiler) sends a poll to the receiving device. The response can be a read command to request data or a write command to send data. At the end of the transmission a poll response is sent from the client (` `) to say we're all done and free up the bus for other clients. -When doing a request to read data the `[src]` is our device `(0x0B)` and the `[dest]` must have has it's MSB (8th bit) set. Say we were requesting data from the thermostat we would use `[dest] = 0x97` since RC30 has an ID of 0x17. +When executing a request to read data the `[src]` is our device `(0x0B)` and the `[dest]` must have has it's MSB (8th bit) set. Say we were requesting data from the thermostat we would use `[dest] = 0x97` since RC20 has an ID of 0x17. Following a write request, the `[dest]` doesn't have the 8th bit set and after this write request the destination device will send either a single byte 0x01 for success or 0x04 for failure. -Every telegram sent is echo'd back to Rx. +Every telegram sent is echo'd back to Rx, along the same Bus used for all Rx/Tx transmissions. ## The ESP8266 Source Code -*Disclaimer*: This code here is really for reference only, I don't expect anyone to use "as is" since it's highly tailored to my environment and my needs. Most of the code however is self explanatory with comments here and there in the code. +`emsuart.cpp` handles the low level UART read and write logic to the bus. You shouldn't need to touch this. All receive commands from the EMS bus are handled asynchronously using a circular buffer via an interrupt. A separate function processes the buffer and extracts the telegrams. -The code is built on the Arduino framework and is dependent on these external libraries: +`ems.cpp` is the logic to read the EMS data packets (telegrams), validates them and process them based on the type. -- Time http://playground.arduino.cc/code/time -- PubSubClient http://pubsubclient.knolleary.net -- ArduinoJson https://github.com/bblanchon/ArduinoJson -- CRC32 https://github.com/bakercp/CRC32 +`ems-esp.ino` is the Arduino code for the ESP8266 that kicks it all off. This is where we have specific logic such as the code to monitor and alert on the Shower timer and light up the LEDs. -`emsuart.cpp` handles the low level UART read and write logic. You shouldn't need to touch this. All receive commands from the EMS bus are handled asynchronously using a circular buffer via an interrupt. A separate function processes the buffer and extracts the telegrams. Since we don't send too many write commands this is done sequentially. I couldn't use the standard Arduino Serial implementation because of the 11-bit break signal causes a frame-error which gets ignored. +`my_config.h` has all the custom settings tailored to your environment. Specific values here are also stored in the ESP's SPIFFs (File system). -`ems.cpp` is the logic to read the EMS packets (telegrams), validates them and process them based on the type. +`ems_devices.h` has all the configuration for the known EMS devices currently supported. -`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. +`MyESP.cpp` is my custom library to handle WiFi, MQTT and Telnet. Uses a modified version of [TelnetSpy](https://github.com/yasheena/telnetspy) -`ESPHelper.cpp` is my customized version of [ESPHelper](https://github.com/ItKindaWorks/ESPHelper) with added Telnet support and some other minor tweaking. - -### Supported EMS Types +### Special EMS Types `ems.cpp` defines callback functions that handle all the broadcast types listed above (e.g. 0x34, 0x18, 0x19 etc) plus these extra types: -| Source (ID) | Type ID | Name | Description | -| ----------------- | ---------------- | ----------------------------- | ---------------------------------------- | -| Boiler (0x08) | 0x33 | UBAParameterWW | reads selected & desired warm water temp | -| Boiler (0x08) | 0x14 | UBATotalUptimeMessage | | -| Boiler (0x08) | 0x15 | UBAMaintenanceSettingsMessage | | -| Boiler (0x08) | 0x16 | UBAParametersMessage | | -| Thermostat (0x17) | 0xA8 | RC20Set | sets operating modes for an RC20 | -| Thermostat (0x10) | 0xA7 | RC30Set | sets operating modes for an RC30 | -| Thermostat | 0x02 | Version | reads Version major/minor | -| Thermostat | 0x91, 0x41, 0x0A | Status Message | read monitor values | +| Source (ID) | Type ID | Name | Description | +| ------------- | ------- | ----------------------------- | ---------------------------------------- | +| Boiler (0x08) | 0x33 | UBAParameterWW | reads selected & desired warm water temp | +| Boiler (0x08) | 0x14 | UBATotalUptimeMessage | | +| Boiler (0x08) | 0x15 | UBAMaintenanceSettingsMessage | | +| Boiler (0x08) | 0x16 | UBAParametersMessage | | -In `boiler.ino` you can make calls to automatically send these read commands. See the function *regularUpdates()* +In `ems.cpp` you can add scheduled calls to specific EMS types in the functions `ems_getThermostatValues()` and `ems_getBoilerValues()`. -### Supported Thermostats +### Which thermostats are supported? -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. Any contributions here are welcome. The know types are listed in `ems_devices.h` and include -#### RC20 (Moduline 300) - -Read and write of setpoint temp and mode supported. - -#### RC30 (Moduline 400) - -Read and write of setpoint temp and mode supported. - -Type's 3F, 49, 53, 5D are identical. So are 4B, 55, 5F and mostly zero's. Types 40, 4A, 54 and 5E are also the same. - -#### 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. -The mode is on type 0x47 (or 0x3D) and the 8th byte (data[7]). 0=off, 1=on, 2=auto - -#### TC100/TC200 (Nefit Easy) - -There is limited support for an Nefit Easy TC100/TC200 type thermostat. The current room temperature and setpoint temperature can be read. What I'm still figuring out is how to read the mode and set the temperature values without sending http post commands to their web server. +- RC20 and RC30, both are fully supported +- RC10 support is being added +- RC35 with support for the 1st heating circuit (HC1) +- TC100/TC200/Easy but only with support for reading the temperatures. There seems to be no way to set settings using EMS bus messages that I know of. One option is to send XMPP messages but a special server is needed and out of scope for this project. ### 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 WIFI and MQTT settings, instead of doing this in `platformio.ini` + - set flags for enabled/disabling functionality such as `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`. + - Set WIFI and MQTT settings. The values can also be set from the telnet command menu using the **set** command. - To add new handlers for EMS data types, first create a callback function and add to the `EMS_Types` array at the top of the file `ems.cpp` and modify `ems.h` +- To add new devices modify `ems_devices.h` ### Using MQTT -When the ESP8266 boots it will send a start signal via MQTT. This is picked up by Home Assistant and sends a notification informing me that the device has booted. Useful for knowing when the ESP gets reset. +The boiler data is collected and sent as a single JSON object to MQTT TOPIC `home/ems-esp/boiler_data`. The `home` preifx is the MQTT topic prefix and can be customized in `my_config.h`. A hash is generated (CRC32 based) to determine if the payload has changed, otherwise it will not be sent. An example payload looks like: -I'm using the standard PubSubClient client so make sure you set `-DMQTT_MAX_PACKET_SIZE=400` as the default package size is 128 and our JSON messages are around 300 bytes. +`{"wWSelTemp":"60","selFlowTemp":"5.0","outdoorTemp":"?","wWActivated":"on","wWComfort":"Comfort","wWCurTmp":"46.0","wWCurFlow":"0.0","wWHeat":"on","curFlowTemp":"54.2","retTemp":"51.5","burnGas":"off","heatPmp":"off","fanWork":"off","ignWork":"off","wWCirc":"off","selBurnPow":"0","curBurnPow":"0","sysPress":"1.2","boilTemp":"56.7","pumpMod":"0","ServiceCode":"0H"}` -I run Mosquitto on my Raspberry PI 3 as the MQTT broker. +Similarly the thermostat values are also sent as a JSON package with the topic `home/ems-esp/thermostat_data` along with the current mode, room temperature and set temperature: -The boiler data is collected and sent as a single JSON object to MQTT TOPIC `home/boiler/boiler_data` (or `{hostname}/boiler_data` for ESPurna). A hash is generated (CRC32 based) to determine if the payload has changed, otherwise don't send it. An example payload looks like: +`{"thermostat_currtemp":"19.8","thermostat_seltemp":"16.0","thermostat_mode":"manual"}` -`{"wWCurTmp":"43.0","wWHeat":"on","curFlowTemp":"51.7","retTemp":"48.0","burnGas":"off","heatPmp":"off","fanWork":"off","ignWork":"off","wWCirc":"off","selBurnPow":"0","curBurnPow":"0","sysPress":"1.6","boilTemp":"54.7","pumpMod":"4"}` +These incoming MQTT topics are also handled: -Similarly the thermostat values are sent as a json package under a topic named `home/boiler/thermostat_data` with the current mode, room temperature and set temperature. +| topic | ID in my_config.h | Payload | Description | +| ------------------- | ------------------------- | ---------------------- | ---------------------------------------- | +| thermostat_cmd_temp | TOPIC_THERMOSTAT_CMD_TEMP | temperature as a float | sets the thermostat current setpoint | +| thermostat_cmd_mode | TOPIC_THERMOSTAT_CMD_MODE | auto, day, night | sets the thermostat mode | +| wwactivated | TOPIC_BOILER_WWACTIVATED | 0 or 1 | turns boiler warm water on/off (not tap) | +| boiler_cmd_wwtemp | TOPIC_BOILER_CMD_WWTEMP | temperature as a float | sets the boiler wwtemp current setpoint | -These topics can be configured in the `TOPIC_*` defines in `boiler.ino`. Make sure you change the HA configuration too to match. +If MQTT is not used use 'set mqtt_host' to remove it. + +Some home automation systems such as Domoticz and OpenHab have special formats for their MQTT messages so I would advise to use [node-red](https://nodered.org/) as a parser like in [this example](https://www.domoticz.com/forum/download/file.php?id=18977&sid=67d048f1b4c8833822175eac6b55ecff). ### The Basic Shower Logic -Checking whether the shower is running was tricky. We know when the warm water is on and being heated but need to distinguish between the central heating, shower, hot tap and bath. I found via trial and error the Selected Burner Max Power is between 80% and 115% when the shower is running and fixed at 75% if the central heating is on. Furthermore the Selected Flow Impulsion is 80 C for the heating. +Checking whether the shower is running is tricky. We know when the warm water is on and being heated but need to distinguish between the central heating, shower, hot tap and even a bath tap. So this code is a little experimental. -There is other logic in the code to compensate for ramp up and whether the shower is turned off and back on again quickly within a 10 second window. +There is other logic in the code to compensate for water heating up to shower temperature and whether the shower is turned off and back on again quickly within a 10 second window. ## Home Assistant Configuration @@ -300,122 +280,85 @@ and the alerts on an iOS/Android device using PushBullet, PushOver or any notifi ![Home Assistant iPhone notify)](doc/home_assistant/ha_notify.jpg) -You can find the .yaml configuration files under `doc/ha`. See also https://community.home-assistant.io/t/thermostat-and-boiler-controller-for-ems-based-boilers-nefit-buderus-bosch-using-esp/53382 +You can find the .yaml configuration files under `doc/ha`. See also this [HA forum post](https://community.home-assistant.io/t/thermostat-and-boiler-controller-for-ems-based-boilers-nefit-buderus-bosch-using-esp/53382). ## Building The Firmware ### Using PlatformIO Standalone -PlatformIO is my preferred way. The code uses a modified version [ESPHelper](https://github.com/ItKindaWorks/ESPHelper) which handles all the basic handling of the WiFi, MQTT, OTA and Telnet server. I switched from Atom to the marvelous Visual Studio Code, works on Windows, OSX and Linux. - **On Windows:** - Download [Git](https://git-scm.com/download/win) (install using the default settings) - Download and install [Visual Studio Code](https://code.visualstudio.com/docs/?dv=win) (VSC). It's like 40MB so don't confuse with the commercial Microsoft Visual Studio. - Restart the PC (if using Windows) to apply the new PATH settings. It should now detect Git -- Install these VSC extensions: PlatformIO IDE & GitLens, and then click reload to activate them +- Install the VSC extension "PlatformIO IDE" then click reload to activate it - Git clone this repo, eith using `git clone` from PlatformIO's terminal or the Git GUI interface -- Create a `platformio.ini` based on the `platformio.ini-example` making the necessary changes for your WiFi and MQTT credentials in the build flags. If you're not using MQTT leave MQTT_IP empty (`MQTT_IP=""`) +- Create a `platformio.ini` based on the `platformio.ini-example` making the necessary changes for your board type -**On Linux (e.g. Ubuntu under Windows10):** +**On Linux (e.g. Ubuntu under Windows 10):** -- make sure Python 2.7 is installed -- make sure you have a Linux distro installed (https://docs.microsoft.com/en-us/windows/wsl/install-win10) -- Do: +Make sure Python 2.7 is installed, then... ```python % pip install -U platformio % sudo platformio upgrade % platformio platform update -% git clone https://github.com/proddy/EMS-ESP-Boiler.git -% cd EMS-ESP-Boiler +% git clone https://github.com/proddy/EMS-ESP.git +% cd EMS-ESP % 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` to your board type, then ```c % platformio run -t upload ``` -### Using ESPurna - -[ESPurna](https://github.com/xoseperez/espurna/wiki) is framework that handles most of the tedious tasks of building IoT devices so you can focus on the functionality you need. This replaces my ESPHelper code in the standalone version above. ESPurna is natively built on PlatformIO and Visual Studio Code too which is nice. The latest version I tested this on is 1.13.3. So if you're brave, follow these steps: - -1. Download and install [NodeJS](https://nodejs.org/en/download). This gives you npm. Choose the LTS version -2. Download ESPurna by cloning the ESPurna git repository (command palette, git clone, https://github.com/xoseperez/espurna.git) -3. Restart VSC. -4. From VSC open the folder `espurna\code`. PlatformIO should detect and set some things up for you automagically -5. open a terminal window (*ctrl-`*) -6. Install the node modules: `npm install --only=dev` -7. Build the web interface: `node node_modules/gulp/bin/gulp.js`. This will create a compressed `code/espurna/static/index.html.gz.h`. If you get warnings about lf during the building edit `gulpfile.js` and change the line `'failOnError': true` to `false` as a temporary workaround. -8. Modify the platformio.ini file making sure you add `-DUSE_CUSTOM_H -DUSE_EXTRA` to the `debug_flags` -9. Copy the following files from EMS-ESP-Boiler repo to where you installed ESPurna -```c -espurna/index.html -> code/html/index.html -espurna/custom.h -> code/config/custom.h -espurna/boiler-espurna.ino -> code/espurna/boiler-espurna.ino -ems*.* -> code/espurna/ -``` -10. Now build and upload as you usually would with PlatformIO (or ctrl-arl-t and choose the right build). Look at my version of platformio.ini as an example. -11. When the firmware loads, use a wifi connected pc/mobile to connect to the Access Point called ESPURNA_XXXXXX. Use 'fibonacci' as the password. Navigate to `http://192.168.4.1` from a browser, set a new username and password when prompted, log off the wifi and reconnect to the AP using these new credentials. Again go to 192.168.4.1 -12. In the ADMIN page enable Telnet and SAVE -13. In the WIFI page add your home wifi details, click SAVE and reboot, and go to the new IP -14. Configure MQTT - -The Telnet functions are `BOILER.READ`, `BOILER.INFO` and a few others for reference. `HELP` will list them. Add your own functions to expand the functionality by calling the EMS* functions as in the examples. - -If you run into issues refer to ESPurna's official setup instructions [here](https://github.com/xoseperez/espurna/wiki/Build-and-update-from-Visual-Studio-Code-using-PlatformIO) and [here](https://github.com/xoseperez/espurna/wiki/Configuration). - -This is what ESPurna looks like with the custom boiler code: - -![Example running in ESPurna](doc/espurna/example.PNG) - -*Note: I didn't bother porting all the EMS Read and Write commands from the Telnet code to the Espurna, but its pretty straight forward if you want to extend the code.* - -### Using Pre-built Firmware - -pre-baked firmwares for some ESP8266 devices based on ESPurna are available in the directory `/firmware` which you can upload yourself using [esptool](https://github.com/espressif/esptool) bootloader. On Windows, follow these instructions: - -1. Check if you have **python 2.7** installed. If not [download it](https://www.python.org/downloads/) and make sure you select the option to add Python to the windows PATH -2. Install the ESPTool by running `pip install esptool` from a command prompt -3. Connect the ESP via USB, figure out the COM port -4. run `esptool.py -p write_flash 0x00000 ` where firmware is the `.bin` file and \ is the COM port, e.g. `COM3` - -now follow the steps in ESPurna section above from #10 on to configure the device. - ### Building Using Arduino IDE -Porting to the Arduino IDE can be a little tricky but it is possible. +Porting to the Arduino IDE can be a little tricky but it did it once. Something along these lines: - Add the ESP8266 boards (from Preferences add Additional Board URL `http://arduino.esp8266.com/stable/package_esp8266com_index.json`) -- Go to Boards Manager and install ESP8266 2.4.x platform +- Go to Boards Manager and install ESP8266 2.4.x platform. Make sure your board supports SPIFFS. - Select your ESP8266 from Tools->Boards and the correct port with Tools->Port -- From the Library Manager install ArduinoJson 5.13.x, PubSubClient 2.6.x, CRC32 and Time -- 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`: +- From the Library Manager install the needed libraries from platformio.ini. Note make sure you pick ArduinoJson v5 (5.13.4 and above) and not v6. See https://arduinojson.org/v5/doc/ +- Put all the files in a single sketch folder +- cross your fingers and hit CTRL-R to compile -```c -#define WIFI_SSID "" -#define WIFI_PASSWORD "" -#define MQTT_IP "" -#define MQTT_USER "" -#define MQTT_PASS "" -``` +## Using the Pre-built Firmware -- Put all the files in a single sketch folder (`ESPHelper.*, boiler.ino, ems.*, emsuart.*`) -- cross your fingers and hit CTRL-R to compile... +pre-baked firmware for the Wemos D1 mini is available in the GitHub [releases](https://github.com/proddy/EMS-ESP/releases) which you can upload yourself using the [esptool](https://github.com/espressif/esptool) bootloader like `esptool.py -p write_flash 0x00000 `. Here's how to set it up on Windows: + +1. Check if you have **python 2.7** installed. If not [download it](https://www.python.org/downloads/) and make sure you select the option to add Python to the windows PATH +2. Then install the ESPTool by running `pip install esptool` from a command prompt + +The ESP8266 will start in Access Point (AP) mode. Connect via WiFi to the SSID **EMS-ESP** and telnet to **192.168.4.1**. Then use the `set wifi` command to configure your own network settings like `set wifi your_ssid your_password`. Alternatively connect the ESP8266 to your PC and open a Serial monitor (with baud 115200) to configure the settings. Make sure you disable Serial support before connecting the EMS lines using `set serial off`. + +`set` wil list all currently stored settings. + +`set erase` will clear all settings. + +## Troubleshooting + +When flashing for the first time the Serial port is enabled by default with baud 115200. You can then use a PC with USB to the ESP8266 to set the settings like wifi, mqtt etc and also monitor the boot up procedure. Remember to disable the serial (`set serial off`) when connecting to the EMS lines. + +The onboard LED will flash if there is no connection with the EMS bus. You can disable LED support by the 'set led' command from the telnet client. + +If you want to completely erase the ESP and rebuild the firmware then do a `pio run -t erase` which will wipe the onboard flash including the SPIFFs where all the settings are stored. ## Known Issues Some annoying issues that need fixing: -- Very infrequently an EMS write command is not sent, probably due to a collision somewhere in the UART between an incoming Rx and a Poll. The retries in the code fix this for now. +- On newer EMS+ Boilers the Tx commands for reading and writing may not always work. I believe there is some handshake that needs to happen before the UBA3/Master is able to send a poll request to our service device. ## Wish List -- 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) +- Measure amount of gas in m3 per day for the hot water vs the central heating, and convert this into cost. +- Support changing temperatures on an Nefit Easy. To do this we must send XMPP messages directly to the thermostat. There is already a TCP stack and a Wifi and Telnet server running in the code, so the building blocks are there to extend with another XMPP client. Here are a number of Python based projects that show how to do this: + - https://github.com/patvdleer/nefit-client-python + - https://github.com/marconfus/ha-nefit + - https://github.com/robertklep/nefit-easy-core +- Improve detection of Heating Off without checking for selFlowTemp (selected flow temperature) +- Split MQTT into smaller chunks. Now the messages can be up to 600 bytes which may cause issues. Preferably make the items configurable. ## Your Comments and Feedback @@ -423,4 +366,4 @@ Any comments, suggestions or code contributions are very welcome. Please post a ## DISCLAIMER -This code and libraries were developed from information gathered on the internet and many hours of reverse engineering the communications between the EMS bus and thermostats. It is **not** based on any official documentation or supported libraries from Buderus/Junkers/Nefit (and associated companies) and therefore there are no guarantees whatsoever regarding the safety of your devices and/or their settings, or the accuracy of the information provided. \ No newline at end of file +This code and libraries were developed from information gathered on the internet and many hours of reverse engineering the communications between the EMS bus and thermostats. It is **not** based on any official documentation or supported libraries from Buderus/Junkers/Nefit (and associated companies) and therefore there are no guarantees whatsoever regarding the safety of your devices and/or their settings, or the accuracy of the information provided. diff --git a/checkcode.py b/checkcode.py new file mode 100644 index 000000000..3e381bb66 --- /dev/null +++ b/checkcode.py @@ -0,0 +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", "lib/myESP"]) + print("\n** Finished cpplint...") + +#my_flags = env.ParseFlags(env['BUILD_FLAGS']) +#defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")} +# print defines +# print env.Dump() + + +# 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/clean_fw.py b/clean_fw.py new file mode 100644 index 000000000..c380f00ac --- /dev/null +++ b/clean_fw.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +from subprocess import call +import os +Import("env") + +def clean(source, target, env): + print("\n** Starting clean...") + call(["pio", "run", "-t", "erase"]) + call(["esptool.py", "-p COM6", "write_flash 0x00000", os.getcwd()+"../firmware/*.bin"]) + print("\n** Finished clean.") + +#my_flags = env.ParseFlags(env['BUILD_FLAGS']) +#defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")} +# print defines +# print env.Dump() + + +# built in targets: (buildprog, size, upload, program, buildfs, uploadfs, uploadfsota) +env.AddPreAction("buildprog", clean) +# 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/doc/Domoticz/nefit/mqtt.py b/doc/Domoticz/nefit/mqtt.py new file mode 100644 index 000000000..21ddc42a5 --- /dev/null +++ b/doc/Domoticz/nefit/mqtt.py @@ -0,0 +1,113 @@ +# Based on https://github.com/emontnemery/domoticz_mqtt_discovery +import Domoticz +import time + +class MqttClient: + Address = "" + Port = "" + mqttConn = None + isConnected = False + mqttConnectedCb = None + mqttDisconnectedCb = None + mqttPublishCb = None + + def __init__(self, destination, port, mqttConnectedCb, mqttDisconnectedCb, mqttPublishCb, mqttSubackCb): + Domoticz.Debug("MqttClient::__init__") + self.Address = destination + self.Port = port + self.mqttConnectedCb = mqttConnectedCb + self.mqttDisconnectedCb = mqttDisconnectedCb + self.mqttPublishCb = mqttPublishCb + self.mqttSubackCb = mqttSubackCb + self.Open() + + def __str__(self): + Domoticz.Debug("MqttClient::__str__") + if (self.mqttConn != None): + return str(self.mqttConn) + else: + return "None" + + def Open(self): + Domoticz.Debug("MqttClient::Open") + if (self.mqttConn != None): + self.Close() + self.isConnected = False + self.mqttConn = Domoticz.Connection(Name=self.Address, Transport="TCP/IP", Protocol="MQTT", Address=self.Address, Port=self.Port) + self.mqttConn.Connect() + + def Connect(self): + Domoticz.Debug("MqttClient::Connect") + if (self.mqttConn == None): + self.Open() + else: + ID = 'Domoticz_'+str(int(time.time())) + Domoticz.Log("MQTT CONNECT ID: '" + ID + "'") + self.mqttConn.Send({'Verb': 'CONNECT', 'ID': ID}) + + def Ping(self): + Domoticz.Debug("MqttClient::Ping") + if (self.mqttConn == None or not self.isConnected): + self.Open() + else: + self.mqttConn.Send({'Verb': 'PING'}) + + def Publish(self, topic, payload, retain = 0): + Domoticz.Log("MqttClient::Publish " + topic + " (" + payload + ")") + if (self.mqttConn == None or not self.isConnected): + self.Open() + else: + self.mqttConn.Send({'Verb': 'PUBLISH', 'Topic': topic, 'Payload': bytearray(payload, 'utf-8'), 'Retain': retain}) + + def Subscribe(self, topics): + Domoticz.Debug("MqttClient::Subscribe") + subscriptionlist = [] + for topic in topics: + subscriptionlist.append({'Topic':topic, 'QoS':0}) + if (self.mqttConn == None or not self.isConnected): + self.Open() + else: + self.mqttConn.Send({'Verb': 'SUBSCRIBE', 'Topics': subscriptionlist}) + + def Close(self): + Domoticz.Log("MqttClient::Close") + #TODO: Disconnect from server + self.mqttConn = None + self.isConnected = False + + def onConnect(self, Connection, Status, Description): + Domoticz.Debug("MqttClient::onConnect") + if (Status == 0): + Domoticz.Log("Successful connect to: "+Connection.Address+":"+Connection.Port) + self.Connect() + else: + Domoticz.Log("Failed to connect to: "+Connection.Address+":"+Connection.Port+", Description: "+Description) + + def onDisconnect(self, Connection): + Domoticz.Log("MqttClient::onDisonnect Disconnected from: "+Connection.Address+":"+Connection.Port) + self.Close() + # TODO: Reconnect? + if self.mqttDisconnectedCb != None: + self.mqttDisconnectedCb() + + def onMessage(self, Connection, Data): + topic = '' + if 'Topic' in Data: + topic = Data['Topic'] + payloadStr = '' + if 'Payload' in Data: + payloadStr = Data['Payload'].decode('utf8','replace') + payloadStr = str(payloadStr.encode('unicode_escape')) + + if Data['Verb'] == "CONNACK": + self.isConnected = True + if self.mqttConnectedCb != None: + self.mqttConnectedCb() + + if Data['Verb'] == "SUBACK": + if self.mqttSubackCb != None: + self.mqttSubackCb() + + if Data['Verb'] == "PUBLISH": + if self.mqttPublishCb != None: + self.mqttPublishCb(topic, Data['Payload']) \ No newline at end of file diff --git a/doc/Domoticz/nefit/plugin.py b/doc/Domoticz/nefit/plugin.py new file mode 100644 index 000000000..abcf78c18 --- /dev/null +++ b/doc/Domoticz/nefit/plugin.py @@ -0,0 +1,165 @@ +""" + + + Plugin to control Nefit EMS-ESP with ' Proddy' firmware
+
+ Automatically creates Domoticz devices for connected device.
+ Do not forget to "Accept new Hardware Devices" on first run
+
+ + + + + + + + +
+""" + +import Domoticz +import json +import time +from mqtt import MqttClient + +class Thermostat: + def checkDevices(self): + if 1 not in Devices: + Domoticz.Debug("Create Temperature Device") + Domoticz.Device(Name="Woonkamer", Unit=1, Type=80, Subtype=5).Create() + if 2 not in Devices: + Domoticz.Debug("Create System Pressure Device") + Domoticz.Device(Name="System Pressure", Unit=2, Type=243, Subtype=9).Create() + if 3 not in Devices: + Domoticz.Debug("Create Thermostat Device") + Domoticz.Device(Name="Nefit", Unit=3, Type=242, Subtype=1).Create() + + def onMqttMessage(self, topic, payload): + if "thermostat_currtemp" in payload: + temp=round(float(payload["thermostat_currtemp"]),1) + Domoticz.Debug("Current temp: {}".format(temp)) + if Devices[1].sValue != temp: + Devices[1].Update(nValue=1, sValue=str(temp)) + if "sysPress" in payload: + pressure=payload["sysPress"] + Domoticz.Debug("System Pressure: {}".format(pressure)) + if Devices[2].sValue != pressure: + Devices[2].Update(nValue=1, sValue=str(pressure)) + if "thermostat_seltemp" in payload: + temp=payload["thermostat_seltemp"] + Domoticz.Debug("Temp setting: {}".format(temp)) + if Devices[3].sValue != temp: + Devices[3].Update(nValue=1, sValue=str(temp)) + + def onCommand(self, mqttClient, unit, command, level, color): + topic = "home/ems-esp/thermostat_cmd_temp" + if (command == "Set Level"): + mqttClient.Publish(topic, str(level)) + +class BasePlugin: + mqttClient = None + + def onStart(self): + self.debugging = Parameters["Mode6"] + + if self.debugging == "Verbose+": + Domoticz.Debugging(2+4+8+16+64) + if self.debugging == "Verbose": + Domoticz.Debugging(2+4+8+16+64) + if self.debugging == "Debug": + Domoticz.Debugging(2+4+8) + + self.controller = Thermostat() + + self.controller.checkDevices() + + self.topics = list(["home/ems-esp/thermostat_data", "home/ems-esp/boiler_data", "home/ems-esp/STATE"]) + self.mqttserveraddress = Parameters["Address"].replace(" ", "") + self.mqttserverport = Parameters["Port"].replace(" ", "") + self.mqttClient = MqttClient(self.mqttserveraddress, self.mqttserverport, self.onMQTTConnected, self.onMQTTDisconnected, self.onMQTTPublish, self.onMQTTSubscribed) + + def checkDevices(self): + Domoticz.Log("checkDevices called") + + def onStop(self): + Domoticz.Log("onStop called") + + def onCommand(self, Unit, Command, Level, Color): + Domoticz.Debug("Command: " + Command + " (" + str(Level)) + self.controller.onCommand(self.mqttClient, Unit, Command, Level, Color) + + def onConnect(self, Connection, Status, Description): + self.mqttClient.onConnect(Connection, Status, Description) + + def onDisconnect(self, Connection): + self.mqttClient.onDisconnect(Connection) + + def onMessage(self, Connection, Data): + self.mqttClient.onMessage(Connection, Data) + + def onHeartbeat(self): + Domoticz.Debug("Heartbeating...") + + # Reconnect if connection has dropped + if self.mqttClient.mqttConn is None or (not self.mqttClient.mqttConn.Connecting() and not self.mqttClient.mqttConn.Connected() or not self.mqttClient.isConnected): + Domoticz.Debug("Reconnecting") + self.mqttClient.Open() + else: + self.mqttClient.Ping() + + def onMQTTConnected(self): + Domoticz.Debug("onMQTTConnected") + self.mqttClient.Subscribe(self.topics) + + def onMQTTDisconnected(self): + Domoticz.Debug("onMQTTDisconnected") + + def onMQTTSubscribed(self): + Domoticz.Debug("onMQTTSubscribed") + + def onMQTTPublish(self, topic, rawmessage): + Domoticz.Debug("MQTT message: " + topic + " " + str(rawmessage)) + + message = "" + try: + message = json.loads(rawmessage.decode('utf8')) + except ValueError: + message = rawmessage.decode('utf8') + + if (topic in self.topics): + self.controller.onMqttMessage(topic, message) + +global _plugin +_plugin = BasePlugin() + +def onStart(): + global _plugin + _plugin.onStart() + +def onStop(): + global _plugin + _plugin.onStop() + +def onConnect(Connection, Status, Description): + global _plugin + _plugin.onConnect(Connection, Status, Description) + +def onDisconnect(Connection): + global _plugin + _plugin.onDisconnect(Connection) + +def onMessage(Connection, Data): + global _plugin + _plugin.onMessage(Connection, Data) + +def onCommand(Unit, Command, Level, Color): + global _plugin + _plugin.onCommand(Unit, Command, Level, Color) + +def onHeartbeat(): + global _plugin + _plugin.onHeartbeat() diff --git a/doc/Domoticz/readme.txt b/doc/Domoticz/readme.txt new file mode 100644 index 000000000..0f39859be --- /dev/null +++ b/doc/Domoticz/readme.txt @@ -0,0 +1,10 @@ +to install the plugin: +- copy the directory 'nefit' to the domoticz/plugins directory +- make sure that 'Accept new Hardware Devices' is enabeled in settings/sysem +- create new hardware with type 'Nefit EMS-ESP with Proddy firmware' +- set MQTT server and port + +The plugin crrently creates 3 devices: +- a room temperature meter +- a system pressure meter +- a thermostat setpoint control \ No newline at end of file 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/automations.yaml b/doc/home_assistant/automations.yaml index afa295b75..f277824f8 100644 --- a/doc/home_assistant/automations.yaml +++ b/doc/home_assistant/automations.yaml @@ -2,7 +2,7 @@ alias: Alert shower time trigger: platform: mqtt - topic: home/boiler/showertime + topic: home/ems-esp/showertime action: - service: notify.general_notify data_template: @@ -13,7 +13,7 @@ alias: Alert shower too long trigger: platform: mqtt - topic: home/boiler/command + topic: home/ems-esp/command payload: 'shower_alarm' action: - service: notify.admin_notify @@ -21,21 +21,21 @@ title: "Shower Alert!" message: "Shower time exceeded limit" -# when boiler starts send boottime +# when ems-esp starts send boottime - id: boiler_restart - alias: See if boiler restarts + alias: See if ems-esp restarts trigger: platform: mqtt - topic: home/boiler/start + topic: home/ems-esp/start payload: 'start' action: - service: notify.admin_notify data_template: - title: "boiler has booted" - message: "Boiler" + title: "ems-esp has booted" + message: "EMS-ESP" - service: mqtt.publish data_template: - topic: 'home/boiler/start' + topic: 'home/ems-esp/start' payload: > {{ now().strftime("%H:%M:%S %-d/%b/%Y") }} diff --git a/doc/home_assistant/binary_sensor.yaml b/doc/home_assistant/binary_sensor.yaml index 0fadb8d19..4b5195d00 100644 --- a/doc/home_assistant/binary_sensor.yaml +++ b/doc/home_assistant/binary_sensor.yaml @@ -1,12 +1,12 @@ - platform: mqtt name: 'Tap Water' - state_topic: 'home/boiler/tapwater_active' + state_topic: 'home/ems-esp/tapwater_active' payload_on: "1" payload_off: "0" - platform: mqtt name: 'Heating' - state_topic: 'home/boiler/heating_active' + state_topic: 'home/ems-esp/heating_active' payload_on: "1" payload_off: "0" \ No newline at end of file diff --git a/doc/home_assistant/climate.yaml b/doc/home_assistant/climate.yaml index 392021b80..18799bc19 100644 --- a/doc/home_assistant/climate.yaml +++ b/doc/home_assistant/climate.yaml @@ -1,20 +1,30 @@ - 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/ems-esp/thermostat_data" + current_temperature_topic: "home/ems-esp/thermostat_data" + temperature_state_topic: "home/ems-esp/thermostat_data" - temperature_command_topic: "home/boiler/thermostat_cmd_temp" - mode_command_topic: "home/boiler/thermostat_cmd_mode" + temperature_command_topic: "home/ems-esp/thermostat_cmd_temp" + mode_command_topic: "home/ems-esp/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 + - platform: mqtt + name: boiler + min_temp: 40 + max_temp: 60 + temp_step: 1 + current_temperature_topic: "home/ems-esp/boiler_data" + temperature_state_topic: "home/ems-esp/boiler_data" + temperature_command_topic: "home/ems-esp/boiler_cmd_wwtemp" + current_temperature_template: "{{ value_json.wWCurTmp }}" + temperature_state_template: "{{ value_json.wWSelTemp }}" diff --git a/doc/home_assistant/ha.png b/doc/home_assistant/ha.png index 632ac8638..c2fcb7b5c 100644 Binary files a/doc/home_assistant/ha.png and b/doc/home_assistant/ha.png differ diff --git a/doc/home_assistant/scripts.yaml b/doc/home_assistant/scripts.yaml index 5134827e5..860d6a2c9 100644 --- a/doc/home_assistant/scripts.yaml +++ b/doc/home_assistant/scripts.yaml @@ -2,6 +2,6 @@ shower_coldshot: sequence: - service: mqtt.publish data_template: - topic: 'home/boiler/shower_coldshot' + topic: 'home/ems-esp/shower_coldshot' payload: '1' diff --git a/doc/home_assistant/sensors.yaml b/doc/home_assistant/sensors.yaml index ecbe341a1..9bc626fba 100644 --- a/doc/home_assistant/sensors.yaml +++ b/doc/home_assistant/sensors.yaml @@ -1,132 +1,138 @@ - platform: mqtt - state_topic: 'home/boiler/thermostat_data' + state_topic: 'home/ems-esp/thermostat_data' name: 'Current Room Temperature' unit_of_measurement: '°C' value_template: "{{ value_json.thermostat_currtemp }}" - platform: mqtt - state_topic: 'home/boiler/thermostat_data' + state_topic: 'home/ems-esp/thermostat_data' name: 'Current Set Temperature' unit_of_measurement: '°C' value_template: "{{ value_json.thermostat_seltemp }}" - platform: mqtt - state_topic: 'home/boiler/thermostat_data' + state_topic: 'home/ems-esp/thermostat_data' name: 'Current Mode' value_template: "{{ value_json.thermostat_mode }}" -# last time boiler was started +# last time esp-esp was started - platform: template sensors: boiler_boottime: value_template: '{{ as_timestamp(states.automation.see_if_boiler_restarts.attributes.last_triggered) | timestamp_custom("%H:%M:%S %d/%m/%y") }}' - platform: mqtt - state_topic: 'home/boiler/showertime' + state_topic: 'home/ems-esp/showertime' name: 'Last shower duration' force_update: true - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Tap Water' value_template: '{{ value_json.tapwaterActive }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Heating' value_template: '{{ value_json.heatingActive }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Warm Water selected temperature' unit_of_measurement: '°C' value_template: '{{ value_json.wWSelTemp }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' + name: 'Warm Water tapwater flow rate' + unit_of_measurement: 'l/min' + value_template: '{{ value_json.wWCurFlow }}' + +- platform: mqtt + state_topic: 'home/ems-esp/boiler_data' name: 'Warm Water current temperature' unit_of_measurement: '°C' value_template: '{{ value_json.wWCurTmp }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Warm Water activated' value_template: '{{ value_json.wWActivated }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Warm Water 3-way valve' value_template: '{{ value_json.wWHeat }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Current flow temperature' unit_of_measurement: '°C' value_template: '{{ value_json.curFlowTemp }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Return temperature' unit_of_measurement: '°C' value_template: '{{ value_json.retTemp }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Gas' value_template: '{{ value_json.burnGas }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Boiler pump' value_template: '{{ value_json.heatPmp }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Fan' value_template: '{{ value_json.fanWork }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Ignition' value_template: '{{ value_json.ignWork }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Circulation pump' value_template: '{{ value_json.wWCirc }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Burner max power' unit_of_measurement: '%' value_template: '{{ value_json.selBurnPow }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Burner max power' unit_of_measurement: '%' value_template: '{{ value_json.selBurnPow }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Burner current power' unit_of_measurement: '%' value_template: '{{ value_json.curBurnPow }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'System Pressure' unit_of_measurement: 'bar' value_template: '{{ value_json.sysPress }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Boiler temperature' unit_of_measurement: '°C' value_template: '{{ value_json.boilTemp }}' - platform: mqtt - state_topic: 'home/boiler/boiler_data' + state_topic: 'home/ems-esp/boiler_data' name: 'Pump modulation' unit_of_measurement: '%' value_template: '{{ value_json.pumpMod }}' @@ -137,7 +143,6 @@ value_template: '{{ as_timestamp(states.sensor.last_shower_duration.last_updated) | int | timestamp_custom("%-I:%M %P on %a %-d %b") }}' boiler_updated: -# value_template: '{{ (as_timestamp(now()) - as_timestamp(states.sensor.boiler_temperature.last_updated)) | int | timestamp_custom("%-M min %-S seconds ago") }}' value_template: '{{ as_timestamp(states.sensor.boiler_temperature.last_updated) | timestamp_custom("%H:%M on %d/%b") }}' diff --git a/doc/home_assistant/switches.yaml b/doc/home_assistant/switches.yaml index 2c667efa7..0649e3c97 100644 --- a/doc/home_assistant/switches.yaml +++ b/doc/home_assistant/switches.yaml @@ -1,7 +1,7 @@ - platform: mqtt name: "Shower Timer" - state_topic: "home/boiler/shower_timer" - command_topic: "home/boiler/shower_timer" + state_topic: "home/ems-esp/shower_timer" + command_topic: "home/ems-esp/shower_timer" payload_on: "1" payload_off: "0" optimistic: false @@ -10,8 +10,8 @@ - platform: mqtt name: "Long Shower Alert" - state_topic: "home/boiler/shower_alert" - command_topic: "home/boiler/shower_alert" + state_topic: "home/ems-esp/shower_alert" + command_topic: "home/ems-esp/shower_alert" payload_on: "1" payload_off: "0" optimistic: false diff --git a/doc/home_assistant/ui-lovelace.yaml b/doc/home_assistant/ui-lovelace.yaml index e144c7344..59f96d36e 100644 --- a/doc/home_assistant/ui-lovelace.yaml +++ b/doc/home_assistant/ui-lovelace.yaml @@ -13,7 +13,8 @@ views: - sensor.warm_water_selected_temperature - sensor.warm_water_current_temperature - sensor.warm_water_activated - - sensor.warm_water_3way_valve + - sensor.warm_water_3_way_valve + - sensor.warm_water_tapwater_flow_rate - type: divider - sensor.boiler_temperature - sensor.return_temperature @@ -38,16 +39,15 @@ views: - type: divider - sensor.last_shower_duration - sensor.showertime_time - - type: custom:button-card - color: auto + - type: entity-button icon: mdi:shower-head name: send a cold shot of shower water - style: - - text-transform: none - - color: rgb(28, 128, 199) - - font-weight: bold entity: script.shower_coldshot - show_state: false + tap_action: + action: call-service + service: script.turn_on + service_data: + entity_id: script.shower_coldshot - type: vertical-stack cards: @@ -57,3 +57,6 @@ views: - sensor.dark_sky_temperature - type: thermostat entity: climate.thermostat + - type: thermostat + name: WarmWater + entity: climate.boiler diff --git a/doc/schematics/Schematic_EMS-ESP-Boiler-supercap.png b/doc/schematics/Schematic_EMS-ESP-supercap.png similarity index 100% rename from doc/schematics/Schematic_EMS-ESP-Boiler-supercap.png rename to doc/schematics/Schematic_EMS-ESP-supercap.png diff --git a/doc/telnet/telnet_example.jpg b/doc/telnet/telnet_example.jpg deleted file mode 100644 index adfb76f1e..000000000 Binary files a/doc/telnet/telnet_example.jpg and /dev/null differ diff --git a/doc/telnet/telnet_menu.jpg b/doc/telnet/telnet_menu.jpg new file mode 100644 index 000000000..9355e493f Binary files /dev/null and b/doc/telnet/telnet_menu.jpg differ diff --git a/doc/telnet/telnet_stats.PNG b/doc/telnet/telnet_stats.PNG index f37c1f6ff..892d66f74 100644 Binary files a/doc/telnet/telnet_stats.PNG and b/doc/telnet/telnet_stats.PNG differ diff --git a/doc/telnet/telnet_verbose.PNG b/doc/telnet/telnet_verbose.PNG index 4a8099155..8f21b95dd 100644 Binary files a/doc/telnet/telnet_verbose.PNG and b/doc/telnet/telnet_verbose.PNG differ diff --git a/espurna/boiler-espurna.ino b/espurna/boiler-espurna.ino deleted file mode 100644 index 14c0bd602..000000000 --- a/espurna/boiler-espurna.ino +++ /dev/null @@ -1,466 +0,0 @@ -// Boiler -// Espurna version -// Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler - -#include "emsuart.h" -#include -#include - -#define myDebug(...) debugSend(__VA_ARGS__) - -#define BOILER_THERMOSTAT_ENABLED 1 -#define BOILER_SHOWER_ENABLED 1 -#define BOILER_POLLING_ENABLED 0 -#define BOILER_LOGGING_NONE 1 - -// home/boiler/ -#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values -#define TOPIC_THERMOSTAT_TEMP "thermostat_temp" // for received thermostat temp changes -#define TOPIC_THERMOSTAT_CURRTEMP "thermostat_currtemp" // current temperature -#define TOPIC_THERMOSTAT_SELTEMP "thermostat_seltemp" // selected temperature -#define TOPIC_THERMOSTAT_MODE "thermostat_mode" // selected temperature -#define TOPIC_BOILER_WARM_WATER_SELECTED_TEMPERATURE "boiler_wwtemp" // warm water selected temp - -#define BOILERSEND_INTERVAL 60000 // send every minute to HA - -typedef struct { - bool wifi_connected; - bool boiler_online; - bool thermostat_enabled; - bool shower_timer; // true if we want to report back on shower times - bool shower_alert; // true if we want the cold water reminder -} _Boiler_Status; - -typedef struct { - bool showerOn; - bool hotWaterOn; - unsigned long timerStart; // ms - unsigned long timerPause; // ms - unsigned long duration; // ms - bool doingColdShot; // true if we've just sent a jolt of cold water -} _Boiler_Shower; - -// store for overall system status -_Boiler_Status Boiler_Status; -_Boiler_Shower Boiler_Shower; - -// Config -void _boilerConfigure() { - Boiler_Status.thermostat_enabled = getSetting("boilerThermostat", BOILER_THERMOSTAT_ENABLED).toInt() == 1; - ems_setThermostatEnabled(Boiler_Status.thermostat_enabled); - - bool _boilerPolling = getSetting("boilerPolling", BOILER_POLLING_ENABLED).toInt() == 1; - ems_setPoll(_boilerPolling); - - uint8_t _boilerLogging = getSetting("boilerLogging", BOILER_LOGGING_NONE).toInt(); - ems_setLogging((_EMS_SYS_LOGGING)_boilerLogging); - - Boiler_Status.shower_timer = getSetting("boilerShower", BOILER_SHOWER_ENABLED).toInt() == 1; -} - -// WEB callbacks -bool _boilerWebSocketOnReceive(const char * key, JsonVariant & value) { - return (strncmp(key, "boiler", 6) == 0); -} - -void _boilerWebSocketOnSend(JsonObject & root) { - root["boilerThermostat"] = getSetting("boilerThermostat", BOILER_THERMOSTAT_ENABLED).toInt() == 1; - root["boilerShower"] = getSetting("boilerShower", BOILER_SHOWER_ENABLED).toInt() == 1; - root["boilerPolling"] = getSetting("boilerPolling", BOILER_POLLING_ENABLED).toInt() == 1; - root["boilerLogging"] = getSetting("boilerLogging", BOILER_LOGGING_NONE).toInt(); -} - -// used if we have a button -void _boilerWebSocketOnAction(uint32_t client_id, const char * action, JsonObject & data) { -} - -// convert float to char -//char * _float_to_char(char * a, float f, uint8_t precision = 1); -char * _float_to_char(char * a, float f, uint8_t precision = 1) { - long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; - - char * ret = a; - // check for 0x8000 (sensor missing), which has a -1 value - if (f == EMS_VALUE_FLOAT_NOTSET) { - strcpy(ret, "?"); - } else { - long whole = (long)f; - itoa(whole, a, 10); - while (*a != '\0') - a++; - *a++ = '.'; - long decimal = abs((long)((f - whole) * p[precision])); - itoa(decimal, a, 10); - } - return ret; -} - -// convert bool to text -char * _bool_to_char(char * s, uint8_t value) { - if (value == EMS_VALUE_INT_ON) { - strcpy(s, "on"); - } else if (value == EMS_VALUE_INT_OFF) { - strcpy(s, "off"); - } else { - strcpy(s, "?"); - } - return s; -} - -// convert int to text value -char * _int_to_char(char * s, uint8_t value) { - if (value == EMS_VALUE_INT_NOTSET) { - strcpy(s, "?"); - } else { - itoa(value, s, 10); - } - return s; -} - -// takes a float value at prints it to debug log -void _renderFloatValue(const char * prefix, const char * postfix, float value) { - myDebug(" %s: ", prefix); - char s[20]; - myDebug("%s", _float_to_char(s, value)); - - if (postfix != NULL) { - myDebug(" %s", postfix); - } - - myDebug("\n"); -} - -// takes an int 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)); - - if (postfix != NULL) { - myDebug(" %s", postfix); - } - - myDebug("\n"); -} - -// 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)); -} - -// 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 "); - _EMS_SYS_LOGGING sysLog = ems_getLogging(); - if (sysLog == EMS_SYS_LOGGING_BASIC) { - myDebug("Basic"); - } else if (sysLog == EMS_SYS_LOGGING_VERBOSE) { - myDebug("Verbose"); - } else { - myDebug("None"); - } - - myDebug("\n # EMS type handlers: %d\n", ems_getEmsTypesCount()); - - myDebug(" Thermostat is %s, Poll is %s, Tx is %s, Shower Timer is %s, Shower Alert is %s\n", - ((Boiler_Status.thermostat_enabled) ? "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, ", - (Boiler_Status.boiler_online ? "yes" : "no"), - EMS_Sys_Status.emsRxPgks, - EMS_Sys_Status.emsTxPkgs, - EMS_Sys_Status.emxCrcErr); - - myDebug("Rx Status="); - switch (EMS_Sys_Status.emsRxStatus) { - case EMS_RX_IDLE: - myDebug("idle"); - break; - case EMS_RX_ACTIVE: - myDebug("active"); - break; - } - - myDebug(", Tx Status="); - switch (EMS_Sys_Status.emsTxStatus) { - case EMS_TX_IDLE: - myDebug("idle"); - break; - case EMS_TX_PENDING: - myDebug("pending"); - break; - case EMS_TX_ACTIVE: - myDebug("active"); - break; - } - - myDebug(", Last Tx Action="); - switch (EMS_TxTelegram.action) { - case EMS_TX_READ: - myDebug("read"); - break; - case EMS_TX_WRITE: - myDebug("write"); - break; - case EMS_TX_VALIDATE: - myDebug("validate"); - break; - case EMS_TX_NONE: - myDebug("none"); - break; - } - - myDebug("\n\n%sBoiler stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); - - // UBAParameterWW - _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); - _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); - _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); - _renderBoolValue("Warm Water 3-way valve", EMS_Boiler.wWHeat); - - // UBAMonitorFast - _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); - _renderFloatValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); - _renderFloatValue("Return temperature", "C", EMS_Boiler.retTemp); - _renderBoolValue("Gas", EMS_Boiler.burnGas); - _renderBoolValue("Boiler pump", EMS_Boiler.heatPmp); - _renderBoolValue("Fan", EMS_Boiler.fanWork); - _renderBoolValue("Ignition", EMS_Boiler.ignWork); - _renderBoolValue("Circulation pump", EMS_Boiler.wWCirc); - _renderIntValue("Burner selected max power", "%", EMS_Boiler.selBurnPow); - _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); - _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); - _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); - - // 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); - - // Thermostat stats - if (Boiler_Status.thermostat_enabled) { - myDebug("\n%sThermostat stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" Thermostat time is %02d:%02d:%02d %d/%d/%d\n", - EMS_Thermostat.hour, - EMS_Thermostat.minute, - EMS_Thermostat.second, - EMS_Thermostat.day, - EMS_Thermostat.month, - EMS_Thermostat.year + 2000); - - _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"); - } else if (EMS_Thermostat.mode == 1) { - myDebug("manual\n"); - } else if (EMS_Thermostat.mode == 2) { - myDebug("auto\n"); - } else { - myDebug("?\n"); - } - } - - myDebug("\n"); -} - -// Init telnet commands -void _boilerInitCommands() { - settingsRegisterCommand(F("BOILER.INFO"), [](Embedis * e) { - showInfo(); - DEBUG_MSG(_boilerGetConfig().c_str()); - DEBUG_MSG_P(PSTR("+OK\n")); - }); - - settingsRegisterCommand(F("BOILER.POLLING"), [](Embedis * e) { - if (e->argc < 2) { - DEBUG_MSG_P(PSTR("-ERROR: arg is 0 or 1\n")); - return; - } - - int param = String(e->argv[1]).toInt(); - - bool b = setSetting("boilerPolling", param); - if (b) { - _boilerConfigure(); - } - - wsSend(_boilerWebSocketOnSend); // update web - - DEBUG_MSG_P(PSTR("+OK\n")); - }); - - settingsRegisterCommand(F("BOILER.LOGGING"), [](Embedis * e) { - if (e->argc < 2) { - DEBUG_MSG_P(PSTR("-ERROR: arg is 0, 1 or 2\n")); - return; - } - - int param = String(e->argv[1]).toInt(); - - bool b = setSetting("boilerLogging", param); - if (b) { - _boilerConfigure(); - } - - wsSend(_boilerWebSocketOnSend); // update web - - DEBUG_MSG_P(PSTR("+OK\n")); - }); - - settingsRegisterCommand(F("BOILER.SEND"), [](Embedis * e) { - if (e->argc < 2) { - DEBUG_MSG_P(PSTR("-ERROR: Wrong arguments\n")); - return; - } - int cmd = String(e->argv[1]).toInt(); - DEBUG_MSG_P(PSTR("Sending %d\n"), cmd); - DEBUG_MSG_P(PSTR("+OK\n")); - }); -} - - -// send values to HA via MQTT -void publishValues() { - char s[20]; // for formatting strings - - // Boiler values as one JSON object - StaticJsonBuffer<512> jsonBuffer; - char data[512]; - JsonObject & root = jsonBuffer.createObject(); - - root["wWSelTemp"] = _int_to_char(s, EMS_Boiler.wWSelTemp); - root["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); - root["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); - root["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); - root["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp); - root["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp); - root["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); - root["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); - root["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); - root["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); - root["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); - root["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); - root["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); - root["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); - root["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); - root["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); - - size_t len = root.measureLength(); - root.printTo(data, len + 1); // form the json string - mqttSend(TOPIC_BOILER_DATA, data); - - // handle the thermostat values separately - if (EMS_Sys_Status.emsThermostatEnabled) { - // only send thermostat values if we actually have them - if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) { - return; - } - - mqttSend(TOPIC_THERMOSTAT_CURRTEMP, _float_to_char(s, EMS_Thermostat.curr_roomTemp)); - mqttSend(TOPIC_THERMOSTAT_SELTEMP, _float_to_char(s, EMS_Thermostat.setpoint_roomTemp)); - - // send mode 0=low, 1=manual, 2=auto - if (EMS_Thermostat.mode == 0) { - mqttSend(TOPIC_THERMOSTAT_MODE, "low"); - } else if (EMS_Thermostat.mode == 1) { - mqttSend(TOPIC_THERMOSTAT_MODE, "manual"); - } else { - mqttSend(TOPIC_THERMOSTAT_MODE, "auto"); - } - } -} - - -void _boilerMQTTCallback(unsigned int type, const char * topic, const char * payload) { -} - -// TELNET commands -String _boilerGetConfig() { - String output; - - // get values and print them - output = String("Thermostat is ") + getSetting("boilerThermostat") + String(", Polling is ") - + getSetting("boilerPolling") + String(", Logging is ") + getSetting("boilerLogging") - + String(", Shower is ") + getSetting("boilerShower") + String("\n"); - - return output; -} - -// SETUP -void extraSetup() { - boilerSetup(); -} - -void boilerSetup() { - // configure - _boilerConfigure(); - - wsOnSendRegister(_boilerWebSocketOnSend); - wsOnActionRegister(_boilerWebSocketOnAction); - wsOnReceiveRegister(_boilerWebSocketOnReceive); - - mqttRegister(_boilerMQTTCallback); - - _boilerInitCommands(); // telnet - - // init shower - Boiler_Shower.timerStart = 0; - Boiler_Shower.timerPause = 0; - Boiler_Shower.duration = 0; - Boiler_Shower.doingColdShot = false; - - // ems init values - ems_init(); - - // start uart - emsuart_init(); - - // Register loop - espurnaRegisterLoop(_boilerLoop); - - espurnaRegisterReload([]() { _boilerConfigure(); }); -} - -// LOOP -void _boilerLoop() { - static unsigned long last_boilersend = 0; - if ((last_boilersend == 0) || (millis() - last_boilersend > BOILERSEND_INTERVAL)) { - last_boilersend = millis(); - - // get 0x33 WW values manually - ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_ID_BOILER); - -#if MQTT_SUPPORT - // send MQTT - publishValues(); -#endif - } -} diff --git a/espurna/custom.h b/espurna/custom.h deleted file mode 100644 index f18d8f0ce..000000000 --- a/espurna/custom.h +++ /dev/null @@ -1,25 +0,0 @@ -// export PLATFORMIO_BUILD_FLAGS="'-DUSE_CUSTOM_H'" -// e.g. -// build_flags = -g -DMQTT_MAX_PACKET_SIZE=400 ${env.ESPURNA_FLAGS} -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH -DUSE_CUSTOM_H -DUSE_EXTRA - -#undef EMBEDDED_WEB -#define EMBEDDED_WEB 1 - -//#undef MQTT_TOPIC -//#define MQTT_TOPIC "/{identifier}" -// default is "{hostname}" - -#undef ENABLE_DOMOTICZ -#define ENABLE_DOMOTICZ 0 - -#undef THINGSPEAK_SUPPORT -#define THINGSPEAK_SUPPORT 0 - -#undef SCHEDULER_SUPPORT -#define SCHEDULER_SUPPORT 0 - -#undef ENABLE_FAUXMO -#define ENABLE_FAUXMO 0 - -#undef DEBUG_SERIAL_SUPPORT -#define DEBUG_SERIAL_SUPPORT 0 diff --git a/espurna/index.html b/espurna/index.html deleted file mode 100644 index 306f90374..000000000 --- a/espurna/index.html +++ /dev/null @@ -1,1893 +0,0 @@ - - - - - - ESPurna 0.0.0 - - - - - - - - - - - - - - - - - - - - - -
- -
- -
- -
- -
-

SECURITY

-

Before using this device you have to change the default password for the user admin. - This password will be used for the AP mode hotspot, the web - interface (where you are now) and the over-the-air updates.

-
- -
- -
-
- - - -
- -
- - - -
-
- -
-
- Password must be 8..63 characters (numbers and letters and any of - these special characters: _,.;:~!?@#$%^&*<>\|(){}[]) and have at least - one lowercase and one uppercase or one number.
-
- - -
- -
- -
- -
-
-
- -
- -
- -
- - - - - - - -
- -
- -
-

STATUS

-

Current configuration

-
- -
- -
-
- -
- - -
-
-
- - - -
- - - -
- -
-
- -
- -
-
- - -
- -
Manufacturer
-
- -
Device
-
- -
Chip ID
-
- -
Wifi MAC
-
- -
SDK version
-
- -
Core version
-
- -
Firmware name
-
- -
Firmware version
-
- -
Firmware revision
-
- -
Firmware build date
-
- -
Firmware size
-
- -
Free space
-
- -
- -
- -
Network
-
- -
BSSID
-
- -
Channel
-
- -
RSSI
-
- -
IP
- - -
Free heap
-
- -
Load average
-
%
- -
VCC
-
? mV
- -
MQTT Status
-
- -
NTP Status
-
- -
Current time
-
- -
Uptime
-
- -
Last update
-
? seconds ago
- -
- -
- -
-
-
- -
-
- -
-

GENERAL

-

General configuration values

-
- -
- -
- -
- - -
-
-
- This name will identify this device in your network - (http://<hostname>.local).
- Hostname may contain only the ASCII letters 'a' through 'z' (in a case-insensitive - manner), the digits '0' through '9', and the hyphen ('-'). They can neither start - or end with an hyphen.
- For this setting to take effect you should restart the wifi interface by clicking - the "Reconnect" button. -
-
- -
- - -
-
-
- Delay in milliseconds to detect a double click (from 0 to 1000ms).
- The lower this number the faster the device will respond to button clicks but the - harder it will be to get a double click. - Increase this number if you are having trouble to double click the button. - Set this value to 0 to disable double click. You won't be able to set the device in - AP mode manually but your device will respond immediately to button clicks.
- You will have to reboot the device after updating for this setting - to apply. -
-
- -
- - -
-
-
- This setting defines the behaviour of the main LED in the board.
- When in "WiFi status" it will blink at 1Hz when trying to connect. If successfully - connected it will briefly blink every 5 seconds if in STA mode or every second if - in AP mode.
- When in "Relay status" mode the LED will be ON whenever any relay is ON, and OFF - otherwise. This is global status notification.
- When in "MQTT managed" mode you will be able to set the LED state sending a message - to "<base_topic>/led/0/set" with a payload of 0, 1 or 2 (to toggle it).
- When in "Find me" mode the LED will be ON when all relays are OFF. This is meant to - locate switches at night.
- When in "Relay & WiFi" mode it will follow the WiFi status but will stay mostly - off when relays are OFF, and mostly ON when any of them is ON.
- When in "Find me & WiFi" mode is the opposite of the "Relay & WiFi", it - will follow the WiFi status but will stay mostly on when relays are OFF, and mostly - OFF when any of them is ON.
- "Always ON" and "Always OFF" modes are self-explanatory. -
-
- -
- -
-
- -
-
-
-
- -
-
- -
-

SWITCHES

-

Switch / relay configuration

-
- -
- -
- - General - -
- - -
-
Define how the different switches should be - synchronized.
-
- -
- -
- -
-
-
- - -
-
- -
-

LIGHTS

-

Lights configuration

-
- -
- -
- -
- -
-
-
-
Use the first three channels as RGB channels. - This will also enable the color picker in the web UI. Will only work if the device - has at least 3 dimmable channels.
Reload the page to update the web interface.
-
- -
- -
-
-
-
Use RGB color picker if enabled (plus - brightness), otherwise use HSV (hue-saturation-value) style
-
- -
- -
-
-
-
Use forth dimmable channel as (cold) white - light calculated out of the RGB values.
Will only work if the device has at - least 4 dimmable channels.
Enabling this will render useless the "Channel 4" - slider in the status page.
Reload the page to update the web interface.
-
- -
- -
-
-
-
Use fifth dimmable channel as warm white light - and the forth dimmable channel as cold white.
Will only work if the device has - at least 5 dimmable channels and "white channel" above is also ON.
Enabling - this will render useless the "Channel 5" slider in the status page.
Reload the - page to update the web interface.
-
- -
- -
-
-
-
Use gamma correction for RGB channels.
Will - only work if "use colorpicker" above is also ON.
-
- -
- -
-
-
-
Use CSS style to report colors to MQTT and - REST API.
Red will be reported as "#FF0000" if ON, otherwise "255,0,0"
-
- -
- -
-
-
-
If enabled color changes will be smoothed.
-
- -
- -
-
-
-
Time in millisecons to transition from one - color to another.
-
- -
-
-
-
-
Sync color between different lights.
-
- -
-
-
-
- - -
-
- -
-

ADMINISTRATION

-

Device administration and security settings

-
- -
- -
- -
- -
-
-
-
- -
- - - -
- - - -
-
- The administrator password is used to access this web interface (user 'admin'), but - also to connect to the device when in AP mode or to flash a new firmware - over-the-air (OTA).
- It must be 8..63 characters (numbers and letters and any of these - special characters: _,.;:~!?@#$%^&*<>\|(){}[]) and have at least one - lowercase and one uppercase or one number.
-
- -
- - -
-
-
- This is the port for the web interface and API requests. - If different than 80 (standard HTTP port) you will have to add it explicitly to - your requests: http://myip:myport/ -
-
- -
- -
-
- -
- -
-
- -
- -
-
-
-
- If enabled, API requests to change a status (like a relay) must be done using PUT. - If disabled you can issue them as GET requests (easier from a browser). -
-
- -
- -
-
-
-
- By default, some magnitudes are being preprocessed and filtered to avoid spurious - values. - If you want to get real-time values (not preprocessed) in the API turn on this - setting. -
-
- -
- - -
-
-
- This is the key you will have to pass with every HTTP request to the API, either to - get or write values. - All API calls must contain the apikey parameter with the value - above. - To know what APIs are enabled do a call to /apis. -
-
- -
- -
-
-
-
Turn ON to be able to telnet to your device - while connected to your home router.
TELNET is always enabled in AP mode.
-
- - -
- -
-
- -
- - -
-
This name address of the NoFUSS server for - automatic remote updates (see https://bitbucket.org/xoseperez/nofuss).
-
- -
- - -
-
-
-
The device has - bytes available for OTA updates. If your image is larger than this consider doing a - two-step - update.
-
-
- -
- -
-
-
-
- -
-
- -
-

WIFI

-

You can configure up to 5 different WiFi networks. The device will try to connect in order - of signal strength.

-
- -
- -
- - General - -
- -
-
-
-
- ESPurna will scan for visible WiFi SSIDs and try to connect to networks defined - below in order of signal strength, even if multiple AP share the - same SSID. - When disabled, ESPurna will try to connect to the networks in the same order they - are listed below. - Disable this option if you are connecting to a single access point - (or router) or to a hidden SSID. -
-
- -
-
- -
- -
- - Networks - -
- - - -
-
-
-
- -
-
- -
-

SCHEDULE

-

Turn switches ON and OFF based on the current time.

-
- -
- -
- -
- - - - - - -
- -
- -
-
- - -
-
- -
-

MAPPING

-

- Configure the map between nodeID/key and MQTT topic. Messages from the given nodeID with - the given key will be forwarded to the specified topic. - You can also configure a default topic using {nodeid} and {key} as placeholders, if the - default topic is empty messages without defined map will be discarded. -

-
- -
- -
- - Default topic - -
- -
- - Specific topics - -
- - - -
-
- -
-
- -
-
-
-

MESSAGES

-

- Messages being received. Previous messages are not displayed. - You have to keep the page open in order to keep receiving them. - You can filter/unfilter by clicking on the values. - Left click on a value to show only rows that match that value, middle click to show all - rows but those matching that value. - Filtered colums have red headers. -

-
- -
- - - - - - - - - - - - - - - - - -
TimestampSenderIDPacketIDTargetIDKeyValueRSSIDuplicatesMissing
- - - - - - -
-
-
- - -
-
- -
-

MQTT

-

Configure an MQTT broker in your network and you will be able to change - the switch status via an MQTT message.

-
- -
- -
- -
- -
-
- -
- - -
- -
- - -
- -
- - -
- -
- - - -
- -
- - -
-
-
- If left empty the firmware will generate a client ID based on the serial number of - the chip. -
-
- -
- - -
- -
- -
-
- -
- - -
- -
- -
-
- -
- - -
-
- This is the fingerprint for the SSL certificate of the server.
- You can get it using https://www.grc.com/fingerprints.htm
- or using openssl from a linux box by typing:
-
$ openssl s_client -connect <host>:<port> < /dev/null 2>/dev/null | openssl x509 -fingerprint -noout -in /dev/stdin
-
-
- -
- - -
-
- This is the root topic for this device. The {hostname} and {mac} placeholders will - be replaced by the device hostname and MAC address.
- - <root>/relay/#/set Send a 0 or a 1 as a payload to this - topic to switch it on or off. You can also send a 2 to toggle its current state. - Replace # with the switch ID (starting from 0). If the board has only one switch it - will be 0.
- - - <root>/rgb/set Set the - color using this topic, your can either send an "#RRGGBB" value or - "RRR,GGG,BBB" (0-255 each).
- - <root>/hsv/set Set the - color using hue (0-360), saturation (0-100) and value (0-100) values, comma - separated.
- - <root>/brightness/set - Set the brighness (0-255).
- - <root>/channel/#/set Set - the value for a single color channel (0-255). Replace # with the channel ID - (starting from 0 and up to 4 for RGBWC lights).
- - <root>/mired/set Set the - temperature color in mired.
- - - <root>/status The device will report a 1 to this topic - every few minutes. Upon MQTT disconnecting this will be set to 0.
- - Other values reported (depending on the build) are: firmware and - version, hostname, IP, MAC, - signal strenth (RSSI), uptime (in seconds), - free heap and power supply. -
-
- -
- -
-
-
-
- All messages (except the device status) will be included in a JSON payload along - with the timestamp and hostname - and sent under the <root>/data topic.
- Messages will be queued and sent after 100ms, so different messages could be merged - into a single payload.
- Subscriptions will still be done to single topics. -
-
- -
-
- -
-
- -
-
- -
-

NTP

-

Configure your NTP (Network Time Protocol) servers and local configuration to keep your - device time up to the second for your location.

-
- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- -
- - -
- -
-
- -
-
- -
-
- -
-

DOMOTICZ

-

- Configure the connection to your Domoticz server. -

-
- -
- -
- - General - -
- -
-
- -
- - -
- -
- - -
- - Sensors & actuators - -
-
Set IDX to 0 to disable notifications from that component.
-
- -
- - -
- - -
-
- -
-
- -
-
- -
-

HOME ASSISTANT

-

- Add this device to your Home Assistant. -

-
- -
-
- - Discover - -
- -
-
-
-
- Home Assistant auto-discovery feature. Enable and save to add the device to your HA - console. - - When using a colour light you might want to disable CSS style so Home Assistant can - parse the color. - -
-
- -
- - -
- - Configuration - -
- -
-
-
- These are the settings you should copy to your Home Assistant "configuration.yaml" - file. - If any of the sections below (switch, light, sensor) already exists, do not - duplicate it, - simply copy the contents of the section below the ones already present. -
-
-
- -
- - -
-
- -
-
- -
-
- -
-

THINGSPEAK

-

- Send your sensors data to Thingspeak. -

-
- -
- -
- - General - -
- -
-
- -
- - -
- - Sensors & actuators - -
-
Enter the field number to send each data to, 0 disable - notifications from that component.
-
- -
- - -
- - -
-
- -
-
- -
-
- -
-

INFLUXDB

-

- Configure the connection to your InfluxDB server. Leave the host field empty to disable - InfluxDB connection. -

-
- -
- -
- -
- -
-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - - -
- -
-
- -
-
- -
-
- -
-

DEBUG LOG

-

- Shows debug messages from the device -

-
- -
- -
- -
-
- Write a command and click send to execute it on the device. The output will be - shown in the debug text area below. -
- -
-
- -
- -
-
- -
- -
- -
-
- - -
-
- -
-

SENSOR CONFIGURATION

-

- Configure and calibrate your device sensors. -

-
- -
- -
- - General - -
- - -
-
-
- Select the interval between readings. These will be filtered and averaged for the - report. - Please mind some sensors do not have fast refresh intervals. Check the sensor - datasheet to know the minimum read interval. - The default and recommended value is 6 seconds. -
-
- -
- -
-
-
-
- Select the number of readings to average and report -
-
- -
- -
-
-
-
- Save aggregated data to EEPROM after these many reports. At the moment this only - applies to total energy readings. - Please mind: saving data to EEPROM too often will wear out the flash memory - quickly. - Set it to 0 to disable this feature (default value). -
-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- Temperature correction value is added to the measured value which may be inaccurate - due to many factors. The value can be negative. -
-
- -
- - -
-
-
- Humidity correction value is added to the measured value which may be inaccurate - due to many factors. The value can be negative. -
-
- -
- -
-
-
-
Move this switch to ON and press "Save" to - reset gas sensor calibration. Check the sensor datasheet for calibration - conditions.
-
- - Energy monitor - -
- - -
-
Mains voltage in your system (in V).
-
- -
- - -
-
In Amperes (A). If you are using a pure - resistive load like a bulb, this will be the ratio between the two previous values, - i.e. power / voltage. You can also use a current clamp around one of the power - wires to get this value.
-
- -
- - -
-
In Volts (V). Enter your the nominal AC - voltage for your household or facility, or use multimeter to get this value.
-
- -
- - -
-
In Watts (W). Calibrate your sensor connecting - a pure resistive load (like a bulb) and enter here the its nominal power or use a - multimeter.
-
- -
- -
-
-
-
Move this switch to ON and press "Save" to - revert to factory calibration values.
-
- -
- -
-
-
-
Move this switch to ON and press "Save" to set - energy count to 0.
-
- -
-
- -
-
- - -
-
-
-

BOILER

-

Configure the - BOILER settings. Make sure you Save after changing values.

-
-
-
-
- -
- -
-
-
-
Enable if you want to read/write from - Thermostat
-
- -
- -
- -
-
-
-
Polling for better support and debugging
-
- -
- -
- -
-
-
-
Monitor shower timings
-
- -
- - -
- -
-
-
-
- - -
-
- -
-

RADIO FREQUENCY

-

- Sonoff 433 RF Bridge & RF Link Configuration

- This page allows you to configure the RF codes for the Sonoff RFBridge 433 and also for a - basic RF receiver.

- To learn a new code click LEARN (the Sonoff RFBridge will beep) then press - a button on the remote, the new code should show up (and the RFBridge will double beep). If - the device double beeps but the code does not update it has not been properly learnt. Keep - trying.

- Modify or create new codes manually and then click SAVE to store them in - the device memory. If your controlled device uses the same code to switch ON and OFF, learn - the code with the ON button and copy paste it to the OFF input box, then click SAVE on the - last one to store the value.

- Delete any code clicking the FORGET button. -

You can also specify 116-chars long RAW - codes. Raw codes require a specific - firmware for for the EFM8BB1.
-

-
- -
-
-
-
-
- -
-
- - -
- -
- - - - -
- - Switch # - -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
- - -
- -
- - -
-
- - - - - - - -
-
Leave empty for DHCP negotiation
- - - -
-
Set when using a static IP
- - - -
-
Usually 255.255.255.0 for /24 networks
- - - -
-
Set the Domain Name Server IP to use when using a static IP
- -
- - -
- -
- -
- -
- - -
- -
 h
-
-
- -
 m
-
-
- - -
- -
- -
- -
-
 1 for Monday, 2 for Tuesday...
- -
-
- - -
- -
- -
-
- -
- -
- -
- - -
- - -
- -
- -
- - -
- - -
-
- -
-
-
- -
- Switch # (GPIO) -
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
- - -
-
- -
-
-
-
- - - -
-
- - -
- -
- -
-
- - - -
-
- -
-
- - - -
-
- -
-
- - - -
-
- - - -
-
- -
- -
-
-
-
- - - -
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/firmware/nodemcu2.bin b/firmware/nodemcu2.bin deleted file mode 100644 index 9fed61e04..000000000 Binary files a/firmware/nodemcu2.bin and /dev/null differ diff --git a/firmware/wemos-d1mini.bin b/firmware/wemos-d1mini.bin deleted file mode 100644 index 6368bca9b..000000000 Binary files a/firmware/wemos-d1mini.bin and /dev/null differ diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp new file mode 100644 index 000000000..566a0c887 --- /dev/null +++ b/lib/MyESP/MyESP.cpp @@ -0,0 +1,1178 @@ +/* + * MyESP - my ESP helper class to handle Wifi, MQTT and Telnet + * + * Paul Derbyshire - December 2018 + * + * Ideas borrowed from Espurna https://github.com/xoseperez/espurna + */ + +#include "MyESP.h" + +#define RTC_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0)) + +/* Days in a month */ +static uint8_t RTC_Months[2][12] = { + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, /* Not leap year */ + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} /* Leap year */ +}; + +// constructor +MyESP::MyESP() { + _app_hostname = strdup("MyESP"); + _app_name = strdup("MyESP"); + _app_version = strdup(MYESP_VERSION); + + _boottime = strdup(""); + _load_average = 100; // calculated load average + + _telnetcommand_callback = NULL; + _telnet_callback = NULL; + + _fs_callback = NULL; + _fs_settings_callback = NULL; + + _helpProjectCmds = NULL; + _helpProjectCmds_count = 0; + + _use_serial = false; + _mqtt_host = NULL; + _mqtt_password = NULL; + _mqtt_username = NULL; + _mqtt_retain = false; + _mqtt_keepalive = 300; + _mqtt_will_topic = NULL; + _mqtt_will_online_payload = NULL; + _mqtt_will_offline_payload = NULL; + _mqtt_base = NULL; + _mqtt_topic = NULL; + _mqtt_qos = 0; + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + _mqtt_last_connection = 0; + _mqtt_connecting = false; + + _wifi_password = NULL; + _wifi_ssid = NULL; + _wifi_callback = NULL; + _wifi_connected = false; + + _suspendOutput = false; +} + +MyESP::~MyESP() { + end(); +} + +// end +void MyESP::end() { + SerialAndTelnet.end(); + jw.disconnect(); +} + +// general debug to the telnet or serial channels +void MyESP::myDebug(const char * format, ...) { + if (_suspendOutput) + return; + + 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, ...) { + if (_suspendOutput) + return; + + 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); + + // capture & print timestamp + char timestamp[10] = {0}; + snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); + SerialAndTelnet.print(timestamp); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// use Serial? +bool MyESP::getUseSerial() { + return (_use_serial); +} + +// called when WiFi is connected, and used to start OTA, MQTT +void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { + if ((code == MESSAGE_CONNECTED)) { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + + 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()); + + // start OTA + ArduinoOTA.begin(); // moved to support esp32 + myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT); + + // MQTT Setup + _mqtt_setup(); + + _wifi_connected = true; + + // finally if we don't want Serial anymore, turn it off + if (!_use_serial) { + Serial.println("Disabling serial port"); + Serial.flush(); + Serial.end(); + SerialAndTelnet.setSerial(NULL); + } else { + Serial.println("Using serial port output"); + } + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + } + + if (code == MESSAGE_ACCESSPOINT_CREATED) { + 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()); + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + } + + if (code == MESSAGE_CONNECTING) { + myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_CONNECT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_DISCONNECTED) { + myDebug_P(PSTR("[WIFI] Disconnected")); + _wifi_connected = false; + } +} + +// received MQTT message +// we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT +void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { + if (len == 0) + return; + + char message[len + 1]; + strlcpy(message, (char *)payload, len + 1); + + // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); // enable for debugging + + // topics are in format MQTT_BASE/HOSTNAME/TOPIC + char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / + if (topic_magnitude != nullptr) { + topic = topic_magnitude + 1; + } + + // Send message event to custom service + (_mqtt_callback)(MQTT_MESSAGE_EVENT, topic, message); +} + +// MQTT subscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttSubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.subscribe(_mqttTopic(topic), _mqtt_qos); + myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT unsubscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttUnsubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic)); + myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT Publish +void MyESP::mqttPublish(const char * topic, const char * payload) { + // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), _mqttTopic(topic), payload); + mqttClient.publish(_mqttTopic(topic), _mqtt_qos, _mqtt_retain, payload); +} + +// MQTT onConnect - when a connect is established +void MyESP::_mqttOnConnect() { + myDebug_P(PSTR("[MQTT] Connected")); + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + + _mqtt_last_connection = millis(); + + // say we're alive to the Last Will topic + mqttClient.publish(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_online_payload); + + // call custom function to handle mqtt receives + (_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL); +} + +// MQTT setup +void MyESP::_mqtt_setup() { + if (!_mqtt_host) { + myDebug_P(PSTR("[MQTT] disabled")); + } + + mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); }); + + mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { + myDebug_P(PSTR("[MQTT] TCP Disconnected. Check mqtt logs.")); + (_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")); + } + + // Reset reconnection delay + _mqtt_last_connection = millis(); + _mqtt_connecting = false; + }); + + //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 + 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 +} + +// set the callback function for the OTA onstart +void MyESP::setOTA(ota_callback_f OTACallback) { + _ota_callback = OTACallback; +} + +// OTA callback when the upload process starts +void MyESP::_OTACallback() { + myDebug_P(PSTR("[OTA] Start")); + SerialAndTelnet.handle(); // force flush + if (_ota_callback) { + (_ota_callback)(); // call custom function to handle mqtt receives + } +} + +// OTA Setup +void MyESP::_ota_setup() { + if (!_wifi_ssid) { + return; + } + + //ArduinoOTA.setPort(OTA_PORT); + ArduinoOTA.setHostname(_app_hostname); + + ArduinoOTA.onStart([this]() { _OTACallback(); }); + 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("[OTA] Auth Failed")); + else if (error == OTA_BEGIN_ERROR) + myDebug_P(PSTR("[OTA] Begin Failed")); + else if (error == OTA_CONNECT_ERROR) + myDebug_P(PSTR("[OTA] Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) + myDebug_P(PSTR("[OTA] Receive Failed")); + else if (error == OTA_END_ERROR) + myDebug_P(PSTR("[OTA] End Failed")); + }); +} + +// sets boottime +void MyESP::setBoottime(const char * boottime) { + if (_boottime) { + free(_boottime); + } + _boottime = strdup(boottime); +} + +// Set callback of sketch function to process project messages +void MyESP::setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { + _helpProjectCmds = cmds; // command list + _helpProjectCmds_count = count; // number of commands + _telnetcommand_callback = callback_cmd; // external function to handle commands + _telnet_callback = callback; +} + +void MyESP::_telnetConnected() { + myDebug_P(PSTR("[TELNET] Telnet connection established")); + _consoleShowHelp(); // Show the initial message + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_CONNECT); // call callback + } +} + +void MyESP::_telnetDisconnected() { + myDebug_P(PSTR("[TELNET] Telnet connection closed")); + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_DISCONNECT); // call callback + } +} + +// Initialize the telnet server +void MyESP::_telnet_setup() { + SerialAndTelnet.setWelcomeMsg(""); + SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); }); + SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); }); + SerialAndTelnet.setDebugOutput(false); + SerialAndTelnet.begin(TELNET_SERIAL_BAUD); // default baud is 115200 + + // init command buffer for console commands + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); +} + +// https://stackoverflow.com/questions/43063071/the-arduino-ntp-i-want-print-out-datadd-mm-yyyy +void MyESP::_printBuildTime(unsigned long unix) { + // compensate for summer/winter time and CET. Can't be bothered to work out DST. + // add 3600 to the UNIX time during winter, (3600 s = 1 h), and 7200 during summer (DST). + unix += 3600; // add 1 hour + + uint8_t Day, Month; + + uint8_t Seconds = unix % 60; /* Get seconds from unix */ + unix /= 60; /* Go to minutes */ + uint8_t Minutes = unix % 60; /* Get minutes */ + unix /= 60; /* Go to hours */ + uint8_t Hours = unix % 24; /* Get hours */ + unix /= 24; /* Go to days */ + uint8_t WeekDay = (unix + 3) % 7 + 1; /* Get week day, monday is first day */ + + uint16_t year = 1970; /* Process year */ + while (1) { + if (RTC_LEAP_YEAR(year)) { + if (unix >= 366) { + unix -= 366; + } else { + break; + } + } else if (unix >= 365) { + unix -= 365; + } else { + break; + } + year++; + } + + /* Get year in xx format */ + uint8_t Year = (uint8_t)(year - 2000); + /* Get month */ + for (Month = 0; Month < 12; Month++) { + if (RTC_LEAP_YEAR(year)) { + if (unix >= (uint32_t)RTC_Months[1][Month]) { + unix -= RTC_Months[1][Month]; + } else { + break; + } + } else if (unix >= (uint32_t)RTC_Months[0][Month]) { + unix -= RTC_Months[0][Month]; + } else { + break; + } + } + + Month++; /* Month starts with 1 */ + Day = unix + 1; /* Date starts with 1 */ + + SerialAndTelnet.printf("%02d:%02d:%02d %d/%d/%d", Hours, Minutes, Seconds, Day, Month, Year); +} + +// Show help of commands +void MyESP::_consoleShowHelp() { + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Connected to: %s version %s", _app_name, _app_version); + SerialAndTelnet.println(); + + if (WiFi.getMode() & WIFI_AP) { + SerialAndTelnet.printf("* ESP is in AP mode with SSID %s", jw.getAPSSID().c_str()); + SerialAndTelnet.println(); + } else { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + SerialAndTelnet.printf("* Hostname: %s IP: %s MAC: %s", + hostname.c_str(), + WiFi.localIP().toString().c_str(), + WiFi.macAddress().c_str()); +#ifdef ARDUINO_BOARD + SerialAndTelnet.printf(" Board: %s", ARDUINO_BOARD); +#endif + SerialAndTelnet.printf(" (MyESP v%s)", MYESP_VERSION); + +#ifdef BUILD_TIME + SerialAndTelnet.print(" (Build "); + _printBuildTime(BUILD_TIME); + SerialAndTelnet.print(")"); +#endif + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Connected to WiFi SSID: %s (signal %d%%)", WiFi.SSID().c_str(), getWifiQuality()); + SerialAndTelnet.println(); + SerialAndTelnet.printf("* MQTT is %s", mqttClient.connected() ? "connected" : "disconnected"); + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Boot time: %s", _boottime); + SerialAndTelnet.println(); + } + + SerialAndTelnet.printf("* Free RAM: %d KB Load: %d%%", (ESP.getFreeHeap() / 1024), getSystemLoadAverage()); + SerialAndTelnet.println(); + // for battery power is ESP.getVcc() + + SerialAndTelnet.println(FPSTR("*")); + SerialAndTelnet.println(FPSTR("* Commands:")); + SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit")); + SerialAndTelnet.println(FPSTR("* reboot")); + SerialAndTelnet.println(FPSTR("* set")); + SerialAndTelnet.println(FPSTR("* set wifi [ssid] [password]")); + SerialAndTelnet.println(FPSTR("* set [value]")); + SerialAndTelnet.println(FPSTR("* set erase")); + SerialAndTelnet.println(FPSTR("* set serial")); + + // print custom commands if available. Taken from progmem + if (_telnetcommand_callback) { + // find the longest key length so we can right align it + uint8_t max_len = 0; + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if (strlen(_helpProjectCmds[i].key) > max_len) + max_len = strlen(_helpProjectCmds[i].key); + } + + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + SerialAndTelnet.print(FPSTR("* ")); + SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length + SerialAndTelnet.print(FPSTR(" ")); // padding + } + SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); + } + } + + SerialAndTelnet.println(); // newline +} + +// reset / restart +void MyESP::resetESP() { + myDebug_P(PSTR("* Reboot ESP...")); + end(); +#if defined(ARDUINO_ARCH_ESP32) + ESP.restart(); +#else + ESP.restart(); +#endif +} + +// read next word from string buffer +char * MyESP::_telnet_readWord() { + return (strtok(NULL, ", \n")); +} + +// change setting for 2 params (set ) +void MyESP::_changeSetting2(const char * setting, const char * value1, const char * value2) { + if (strcmp(setting, "wifi") == 0) { + if (_wifi_ssid) + free(_wifi_ssid); + if (_wifi_password) + free(_wifi_password); + _wifi_ssid = NULL; + _wifi_password = NULL; + + if (value1) { + _wifi_ssid = strdup(value1); + } + + if (value2) { + _wifi_password = strdup(value2); + } + + (void)fs_saveConfig(); + SerialAndTelnet.println("WiFi settings changed. Reconnecting..."); + jw.disconnect(); + jw.cleanNetworks(); + jw.addNetwork(_wifi_ssid, _wifi_password); + } +} + +// change settings - always as strings +// messy code but effective since we don't have too many settings +// wc is word count, number of parameters after the 'set' command +void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { + bool ok = false; + + // check for our internal commands first + if (strcmp(setting, "erase") == 0) { + _fs_eraseConfig(); + return; + } else if ((strcmp(setting, "wifi") == 0) && (wc == 1)) { // erase wifi settings + if (_wifi_ssid) + free(_wifi_ssid); + if (_wifi_password) + free(_wifi_password); + _wifi_ssid = NULL; + _wifi_password = NULL; + ok = true; + } else if (strcmp(setting, "mqtt_host") == 0) { + if (_mqtt_host) + free(_mqtt_host); + _mqtt_host = NULL; // just to be sure + if (value) { + _mqtt_host = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_username") == 0) { + if (_mqtt_username) + free(_mqtt_username); + _mqtt_username = NULL; // just to be sure + if (value) { + _mqtt_username = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_password") == 0) { + if (_mqtt_password) + free(_mqtt_password); + _mqtt_password = NULL; // just to be sure + if (value) { + _mqtt_password = strdup(value); + } + ok = true; + } else if (strcmp(setting, "serial") == 0) { + ok = true; + _use_serial = false; + if (value) { + if (strcmp(value, "on") == 0) { + _use_serial = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + _use_serial = false; + ok = true; + } else { + ok = false; + } + } + } else { + // finally check for any custom commands + ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); + } + + if (!ok) { + SerialAndTelnet.println("\nInvalid parameter for set command."); + return; + } + + // check for 2 params + if (value == nullptr) { + SerialAndTelnet.printf("%s setting reset to its default value.", setting); + } else { + // must be 3 params + SerialAndTelnet.printf("%s changed.", setting); + } + SerialAndTelnet.println(); + + (void)fs_saveConfig(); +} + +void MyESP::_telnetCommand(char * commandLine) { + // count the number of arguments + char * str = commandLine; + bool state = false; + unsigned wc = 0; + while (*str) { + if (*str == ' ' || *str == '\n' || *str == '\t') { + state = false; + } else if (state == false) { + state = true; + ++wc; + } + ++str; + } + + // check first for reserved commands + char * temp = strdup(commandLine); // because strotok kills original string buffer + char * ptrToCommandName = strtok((char *)temp, ", \n"); + + // set command + if (strcmp(ptrToCommandName, "set") == 0) { + if (wc == 1) { + SerialAndTelnet.println(); + SerialAndTelnet.println("Stored settings:"); + SerialAndTelnet.printf(" wifi=%s ", (!_wifi_ssid) ? "" : _wifi_ssid); + if (!_wifi_password) { + SerialAndTelnet.print(""); + } else { + for (uint8_t i = 0; i < strlen(_wifi_password); i++) + SerialAndTelnet.print("*"); + } + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_host=%s", (!_mqtt_host) ? "" : _mqtt_host); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_username=%s", (!_mqtt_username) ? "" : _mqtt_username); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_password="); + if (!_mqtt_password) { + SerialAndTelnet.print(""); + } else { + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) + SerialAndTelnet.print("*"); + } + + SerialAndTelnet.println(); + SerialAndTelnet.printf(" serial=%s", (_use_serial) ? "on" : "off"); + + SerialAndTelnet.println(); + + // print custom settings + (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); + + SerialAndTelnet.println(); + SerialAndTelnet.println("Usage: set [value...]"); + } else if (wc == 2) { + char * setting = _telnet_readWord(); + _changeSetting(1, setting, NULL); + } else if (wc == 3) { + char * setting = _telnet_readWord(); + char * value = _telnet_readWord(); + _changeSetting(2, setting, value); + } else if (wc == 4) { + char * setting = _telnet_readWord(); + char * value1 = _telnet_readWord(); + char * value2 = _telnet_readWord(); + _changeSetting2(setting, value1, value2); + } + return; + } + + // reboot command + if ((strcmp(ptrToCommandName, "reboot") == 0) && (wc == 1)) { + resetESP(); + } + + // call callback function + (_telnetcommand_callback)(wc, commandLine); +} + +// handler for Telnet +void MyESP::_telnetHandle() { + SerialAndTelnet.handle(); + + static uint8_t charsRead = 0; + // read asynchronously until full command input + while (SerialAndTelnet.available()) { + char c = SerialAndTelnet.read(); + + SerialAndTelnet.serialPrint(c); // echo to Serial if connected + + switch (c) { + case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF + case '\n': + _command[charsRead] = '\0'; // null terminate our command char array + if (charsRead > 0) { + charsRead = 0; // is static, so have to reset + _suspendOutput = false; + if (_use_serial) { + SerialAndTelnet.serialPrint('\n'); // force newline if in Serial + } + _telnetCommand(_command); + } + break; + + case '\b': // (^H) handle backspace in input: put a space in last char - coded by Simon Arlott + case 0x7F: // (^?) + + if (charsRead > 0) { + _command[--charsRead] = '\0'; + + SerialAndTelnet.write(' '); + SerialAndTelnet.write('\b'); + } + + break; + + case '?': + if (!_suspendOutput) { + _consoleShowHelp(); + } else { + _command[charsRead++] = c; // add it to buffer as its part of the string entered + } + break; + case 0x04: // EOT, CTRL-D + myDebug_P(PSTR("[TELNET] exiting telnet session")); + SerialAndTelnet.disconnectClient(); + break; + default: + _suspendOutput = true; + if (charsRead < TELNET_MAX_COMMAND_LENGTH) { + _command[charsRead++] = c; + } + _command[charsRead] = '\0'; // just in case + break; + } + } +} + +// ensure we have a connection to MQTT broker +void MyESP::_mqttConnect() { + if (!_mqtt_host) + return; // MQTT not enabled + + // Do not connect if already connected or still trying to connect + if (mqttClient.connected() || _mqtt_connecting || (WiFi.status() != WL_CONNECTED)) { + return; + } + + // Check reconnect interval + if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) { + return; + } + + _mqtt_connecting = true; // we're doing a connection + + // 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); + mqttClient.setKeepAlive(_mqtt_keepalive); + mqttClient.setCleanSession(false); + + // last will + if (_mqtt_will_topic) { + //myDebug_P(PSTR("[MQTT] Setting last will topic %s"), _mqttTopic(_mqtt_will_topic)); + mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, + _mqtt_will_offline_payload); // retain always true + } + + if (_mqtt_username && _mqtt_password) { + myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _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::setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback) { + // Check SSID too long or missing + if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > 31) { + _wifi_ssid = NULL; + } else { + _wifi_ssid = strdup(wifi_ssid); + } + + // Check PASS too long + if (!wifi_password || *wifi_ssid == 0x00 || strlen(wifi_password) > 31) { + _wifi_password = NULL; + } else { + _wifi_password = strdup(wifi_password); + } + + // callback + _wifi_callback = callback; +} + +// init MQTT settings +void MyESP::setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback) { + // 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); + } + + // base + if (_mqtt_base) { + free(_mqtt_base); + } + _mqtt_base = strdup(mqtt_base); + + // callback + _mqtt_callback = callback; + + // various mqtt settings + _mqtt_keepalive = mqtt_keepalive; + _mqtt_qos = mqtt_qos; + _mqtt_retain = mqtt_retain; + + // last will + if (!mqtt_will_topic || *mqtt_will_topic == 0x00) { + _mqtt_will_topic = NULL; + } else { + _mqtt_will_topic = strdup(mqtt_will_topic); + } + + if (!mqtt_will_online_payload || *mqtt_will_online_payload == 0x00) { + _mqtt_will_online_payload = NULL; + } else { + _mqtt_will_online_payload = strdup(mqtt_will_online_payload); + } + + if (!mqtt_will_offline_payload || *mqtt_will_offline_payload == 0x00) { + _mqtt_will_offline_payload = NULL; + } else { + _mqtt_will_offline_payload = strdup(mqtt_will_offline_payload); + } +} + +// builds up a topic by prefixing the base and hostname +char * MyESP::_mqttTopic(const char * topic) { + char buffer[MQTT_MAX_TOPIC_SIZE] = {0}; + + strlcpy(buffer, _mqtt_base, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, _app_hostname, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, topic, sizeof(buffer)); + + if (_mqtt_topic) { + free(_mqtt_topic); + } + _mqtt_topic = strdup(buffer); + + return _mqtt_topic; +} + + +// print contents of file +// assumes Serial is open +void MyESP::_fs_printConfig() { + myDebug_P(PSTR("[FS] Contents:")); + + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + if (!configFile) { + Serial.println(F("[FS] Failed to read file for printing")); + return; + } + + while (configFile.available()) { + SerialAndTelnet.print((char)configFile.read()); + } + SerialAndTelnet.println(); + + configFile.close(); +} + +// format File System +void MyESP::_fs_eraseConfig() { + myDebug_P(PSTR("[FS] Erasing settings, please wait a few seconds. ESP will " + "automatically restart when finished.")); + + if (SPIFFS.format()) { + delay(1000); // wait 1 seconds + resetESP(); + } +} + +void MyESP::setSettings(fs_callback_f callback_fs, fs_settings_callback_f callback_settings_fs) { + _fs_callback = callback_fs; + _fs_settings_callback = callback_settings_fs; +} + +// load from spiffs +bool MyESP::_fs_loadConfig() { + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + + size_t size = configFile.size(); + if (size > 1024) { + myDebug_P(PSTR("[FS] Config file size is too large")); + return false; + } else if (size == 0) { + myDebug_P(PSTR("[FS] Failed to open config file")); + // file does not exist, so assume its the first install. Set serial to on + _use_serial = true; + return false; + } + + StaticJsonDocument doc; + JsonObject json = doc.to(); + + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, configFile); + if (error) { + Serial.println(F("[FS] Failed to read file")); + return false; + } + + const char * value; + + value = json["wifi_ssid"]; + _wifi_ssid = (value) ? strdup(value) : NULL; + + value = json["wifi_password"]; + _wifi_password = (value) ? strdup(value) : NULL; + + value = json["mqtt_host"]; + _mqtt_host = (value) ? strdup(value) : NULL; + + value = json["mqtt_username"]; + _mqtt_username = (value) ? strdup(value) : NULL; + + value = json["mqtt_password"]; + _mqtt_password = (value) ? strdup(value) : NULL; + + _use_serial = (bool)json["use_serial"]; + + // callback for loading custom settings + // ok is false if there's a problem loading a custom setting (e.g. does not exist) + bool ok = (_fs_callback)(MYESP_FSACTION_LOAD, json); + + configFile.close(); + + return ok; +} + +// save settings to spiffs +bool MyESP::fs_saveConfig() { + StaticJsonDocument doc; + JsonObject json = doc.to(); + + json["app_version"] = _app_version; + json["wifi_ssid"] = _wifi_ssid; + json["wifi_password"] = _wifi_password; + json["mqtt_host"] = _mqtt_host; + json["mqtt_username"] = _mqtt_username; + json["mqtt_password"] = _mqtt_password; + json["use_serial"] = _use_serial; + + // callback for saving custom settings + (void)(_fs_callback)(MYESP_FSACTION_SAVE, json); + + // if file exists, remove it just to be safe + if (SPIFFS.exists(MYEMS_CONFIG_FILE)) { + // delete it + SPIFFS.remove(MYEMS_CONFIG_FILE); + } + + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); + if (!configFile) { + Serial.println("[FS] Failed to open config file for writing"); + return false; + } + + // Serialize JSON to file + if (serializeJson(json, configFile) == 0) { + Serial.println(F("[FS] Failed to write to file")); + } + + configFile.close(); + + return true; +} + +// init the SPIFF file system and load the config +// if it doesn't exist try and create it +// force Serial for debugging, and turn it off afterwards +void MyESP::_fs_setup() { + if (!SPIFFS.begin()) { + Serial.println("[FS] Failed to mount the file system"); + _fs_eraseConfig(); // fix for ESP32 + return; + } + + // load the config file. if it doesn't exist (function returns false) create it + if (!_fs_loadConfig()) { + // Serial.println("[FS] Re-creating config file"); + fs_saveConfig(); + } + + //_fs_printConfig(); // TODO: for debugging +} + +uint16_t MyESP::getSystemLoadAverage() { + return _load_average; +} + +// calculate load average +void MyESP::_calculateLoad() { + static unsigned long last_loadcheck = 0; + static unsigned long load_counter_temp = 0; + load_counter_temp++; + + if (millis() - last_loadcheck > LOADAVG_INTERVAL) { + static unsigned long load_counter = 0; + static unsigned long load_counter_max = 1; + + load_counter = load_counter_temp; + load_counter_temp = 0; + if (load_counter > load_counter_max) { + load_counter_max = load_counter; + } + _load_average = 100 - (100 * load_counter / load_counter_max); + last_loadcheck = millis(); + } +} + +// return true if wifi is connected +// WL_NO_SHIELD = 255, // for compatibility with WiFi Shield library +// WL_IDLE_STATUS = 0, +// WL_NO_SSID_AVAIL = 1, +// WL_SCAN_COMPLETED = 2, +// WL_CONNECTED = 3, +// WL_CONNECT_FAILED = 4, +// WL_CONNECTION_LOST = 5, +// WL_DISCONNECTED = 6 +bool MyESP::isWifiConnected() { + return (_wifi_connected); +} + +/* + Return the quality (Received Signal Strength Indicator) + of the WiFi network. + Returns a number between 0 and 100 if WiFi is connected. + Returns -1 if WiFi is disconnected. + + High quality: 90% ~= -55dBm + Medium quality: 50% ~= -75dBm + Low quality: 30% ~= -85dBm + Unusable quality: 8% ~= -96dBm +*/ +int MyESP::getWifiQuality() { + if (WiFi.status() != WL_CONNECTED) + return -1; + int dBm = WiFi.RSSI(); + if (dBm <= -100) + return 0; + if (dBm >= -50) + return 100; + return 2 * (dBm + 100); +} + +// register new instance +void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version) { + _app_hostname = strdup(app_hostname); + _app_name = strdup(app_name); + _app_version = strdup(app_version); + + _telnet_setup(); // Telnet setup + _fs_setup(); // SPIFFS setup, do this first to get values + _wifi_setup(); // WIFI setup + _ota_setup(); +} + +/* + * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc + */ +void MyESP::loop() { + _calculateLoad(); + _telnetHandle(); // Telnet/Debugger + + jw.loop(); // WiFi + + // do nothing else until we've got a wifi connection + if (WiFi.getMode() & WIFI_AP) { + return; + } + + ArduinoOTA.handle(); // OTA + _mqttConnect(); // MQTT + + yield(); // ...and breath +} + +MyESP myESP; // create instance diff --git a/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h new file mode 100644 index 000000000..7096d351b --- /dev/null +++ b/lib/MyESP/MyESP.h @@ -0,0 +1,228 @@ +/* + * MyESP.h + * + * Paul Derbyshire - December 2018 + */ + +#pragma once + +#ifndef MyEMS_h +#define MyEMS_h + +#define MYESP_VERSION "1.1.5" + +#include +#include +#include // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127 +#include +#include +#include // https://github.com/xoseperez/justwifi +#include // modified from https://github.com/yasheena/telnetspy + +#if defined(ARDUINO_ARCH_ESP32) +//#include +#include // added for ESP32 +#define ets_vsnprintf vsnprintf // added for ESP32 +#define OTA_PORT 8266 +#else +//#include +#include +#define OTA_PORT 3232 +#endif + +#define MYEMS_CONFIG_FILE "/config.json" + +#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) + +// 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 + +// MQTT +#define MQTT_PORT 1883 // MQTT port +#define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection +#define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt +#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most +#define MQTT_MAX_SIZE 600 // max length of MQTT message +#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT message + +// Internal MQTT events +#define MQTT_CONNECT_EVENT 0 +#define MQTT_DISCONNECT_EVENT 1 +#define MQTT_MESSAGE_EVENT 2 + +// Telnet +#define TELNET_SERIAL_BAUD 115200 +#define TELNET_MAX_COMMAND_LENGTH 80 // length of a command +#define TELNET_EVENT_CONNECT 1 +#define TELNET_EVENT_DISCONNECT 0 + +// 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" +#define COLOR_BOLD_ON "\x1B[1m" +#define COLOR_BOLD_OFF "\x1B[22m" // fixed by Scott Arlott + +// SPIFFS +#define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v5/assistant/ + +typedef struct { + char key[40]; + char description[100]; +} command_t; + +typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION; + +typedef std::function mqtt_callback_f; +typedef std::function wifi_callback_f; +typedef std::function ota_callback_f; +typedef std::function telnetcommand_callback_f; +typedef std::function telnet_callback_f; +typedef std::function fs_callback_f; +typedef std::function fs_settings_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 setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); + bool isWifiConnected(); + + // mqtt + void mqttSubscribe(const char * topic); + void mqttUnsubscribe(const char * topic); + void mqttPublish(const char * topic, const char * payload); + void setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback); + + // OTA + void setOTA(ota_callback_f OTACallback); + + // debug & telnet + void myDebug(const char * format, ...); + void myDebug_P(PGM_P format_P, ...); + void setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback); + bool getUseSerial(); + + // FS + void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); + bool fs_saveConfig(); + + // general + void end(); + void loop(); + void begin(const char * app_hostname, const char * app_name, const char * app_version); + void setBoottime(const char * boottime); + void resetESP(); + uint16_t getSystemLoadAverage(); + int getWifiQuality(); + + + 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 * _mqttTopic(const char * topic); + char * _mqtt_host; + char * _mqtt_username; + char * _mqtt_password; + char * _mqtt_base; + unsigned long _mqtt_keepalive; + unsigned char _mqtt_qos; + bool _mqtt_retain; + char * _mqtt_will_topic; + char * _mqtt_will_online_payload; + char * _mqtt_will_offline_payload; + char * _mqtt_topic; + unsigned long _mqtt_last_connection; + bool _mqtt_connecting; + + // wifi + DNSServer dnsServer; // For Access Point (AP) support + void _wifiCallback(justwifi_messages_t code, char * parameter); + void _wifi_setup(); + wifi_callback_f _wifi_callback; + char * _wifi_ssid; + char * _wifi_password; + bool _wifi_connected; + + // ota + ota_callback_f _ota_callback; + void _ota_setup(); + void _OTACallback(); + + // telnet & debug + TelnetSpy SerialAndTelnet; + void _telnetConnected(); + void _telnetDisconnected(); + void _telnetHandle(); + void _telnetCommand(char * commandLine); + char * _telnet_readWord(); + void _telnet_setup(); + char _command[TELNET_MAX_COMMAND_LENGTH]; // the input command from either Serial or Telnet + command_t * _helpProjectCmds; // Help of commands setted by project + uint8_t _helpProjectCmds_count; // # available commands + void _consoleShowHelp(); + telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands + telnet_callback_f _telnet_callback; // callback for connect/disconnect + void _changeSetting(uint8_t wc, const char * setting, const char * value); + void _changeSetting2(const char * setting, const char * value1, const char * value2); + + // fs + void _fs_setup(); + bool _fs_loadConfig(); + void _fs_printConfig(); + void _fs_eraseConfig(); + + fs_callback_f _fs_callback; + fs_settings_callback_f _fs_settings_callback; + + // general + char * _app_hostname; + char * _app_name; + char * _app_version; + char * _boottime; + bool _suspendOutput; + bool _use_serial; + void _printBuildTime(unsigned long rawTime); + + // load average (0..100) + void _calculateLoad(); + unsigned short int _load_average; +}; + +extern MyESP myESP; + +#endif diff --git a/lib/TelnetSpy/TelnetSpy.cpp b/lib/TelnetSpy/TelnetSpy.cpp new file mode 100644 index 000000000..ea1762fa4 --- /dev/null +++ b/lib/TelnetSpy/TelnetSpy.cpp @@ -0,0 +1,655 @@ +/* + * 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(const char * msg) { + if (welcomeMsg) { + free(welcomeMsg); + } + welcomeMsg = strdup(msg); +} + +void TelnetSpy::setRejectMsg(const 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; +} + +// this still needs some work +bool TelnetSpy::isSerialAvailable(void) { + if (usedSer) { + usedSer->write("test"); + //Wait for four seconds or till data is available on serial, whichever occurs first. + while (usedSer->available() == 0 && millis() < 4000) + ; + + if (usedSer->available() > 0) { + (void)usedSer->read(); // We then clear the input buffer + return true; + } + } + return false; +} + +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; + + // TODO: figure out how to disable system printing for the ESP32 + if (debugOutput) { + actualObject = this; + +#ifdef ESP8266 + os_install_putc1((void *)TelnetSpy_putc); // Set system printing (os_printf) to TelnetSpy + system_set_os_print(true); +#endif + + } else { + if (actualObject == this) { + +#ifdef ESP8266 + system_set_os_print(false); + os_install_putc1((void *)TelnetSpy_ignore_putc); // Ignore system printing +#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::serialPrint(char c) { + if (usedSer) { + usedSer->print(c); + } +} + +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_DISCONNECTED) && (WiFi.getMode() & WIFI_AP)) { + if (usedSer) { + usedSer->println("[TELNET] in AP mode"); // added by Proddy + } + } else 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..1f7b901c7 --- /dev/null +++ b/lib/TelnetSpy/TelnetSpy.h @@ -0,0 +1,281 @@ +/* + * 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 Telnet.\n" +#define TELNETSPY_REJECT_MSG "Telnet: 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(const char * msg); + void setRejectMsg(const 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 serialPrint(char c); + + 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); + + bool isSerialAvailable(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/platformio.ini-example b/platformio.ini-example index 82ee7f9bf..fdbbeb257 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -1,45 +1,37 @@ [platformio] -; change this for your ESP8266 device -env_default = nodemcuv2 -; env_default = d1_mini +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 -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"' +flash_mode = dout +build_flags = -g -w +;build_flags = -g -w -DBUILD_TIME=$UNIX_TIME + +wifi_settings = +; hard code if you prefer. Recommendation is to set from within the app when in Serial or AP mode +;wifi_settings = '-DWIFI_SSID="XXXX"' '-DWIFI_PASSWORD="XXXX"' + lib_deps = - Time - PubSubClient - ArduinoJson CRC32 CircularBuffer - -[env:nodemcuv2] -board = nodemcuv2 -platform = ${common.platform} -framework = arduino -lib_deps = ${common.lib_deps} -build_flags = ${common.build_flags} ${common.build_flags_custom} -upload_speed = 921600 -; comment out next line if using USB and not OTA -upload_port = "boiler." -; examples.... -;upload_port = "boiler" -;upload_port = "boiler.local" -;upload_port = 10.10.10.6 + JustWifi + AsyncMqttClient + ArduinoJson +; https://github.com/bblanchon/ArduinoJson#v5.13.5 + OneWire [env:d1_mini] board = d1_mini platform = ${common.platform} framework = arduino lib_deps = ${common.lib_deps} -build_flags = ${common.build_flags} ${common.build_flags_custom} +build_flags = ${common.build_flags} ${common.wifi_settings} +board_build.flash_mode = ${common.flash_mode} upload_speed = 921600 -; comment out next line if using USB and not OTA -upload_port = "boiler." -; examples.... -;upload_port = "boiler" -;upload_port = "boiler.local" -;upload_port = 10.10.10.6 +monitor_speed = 115200 + +; for OTA comment out these sections +;upload_protocol = espota +;upload_port = ems-esp.local + diff --git a/rename_fw.py b/rename_fw.py new file mode 100644 index 000000000..aad993d99 --- /dev/null +++ b/rename_fw.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +from subprocess import call +import os +Import("env") + +#my_flags = env.ParseFlags(env['BUILD_FLAGS']) +#defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")} +# print defines +# print env.Dump() + +# 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/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 deleted file mode 100644 index e236ad58e..000000000 --- a/src/boiler.ino +++ /dev/null @@ -1,956 +0,0 @@ -/* - * EMS-ESP-Boiler - * Paul Derbyshire - May 2018 - https://github.com/proddy/EMS-ESP-Boiler - * https://community.home-assistant.io/t/thermostat-and-boiler-controller-for-ems-based-boilers-nefit-buderus-bosch-using-esp/53382 - * - * See README for Acknowledgments - */ - -// local libraries -#include "ESPHelper.h" -#include "ems.h" -#include "emsuart.h" -#include "my_config.h" -#include "version.h" - -// public libraries -#include // https://github.com/bblanchon/ArduinoJson -#include // https://github.com/bakercp/CRC32 - -// standard arduino libs -#include // https://github.com/esp8266/Arduino/tree/master/libraries/Ticker - -// timers, all values are in seconds -#define PUBLISHVALUES_TIME 120 // every 2 minutes post HA values -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 -Ticker regularUpdatesTimer; - -#define HEARTBEAT_TIME 1 // every second blink heartbeat LED -Ticker heartbeatTimer; - -// thermostat scan - for debugging -Ticker scanThermostat; -#define SCANTHERMOSTAT_TIME 4 -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 - -// 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 - -// 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 - -// logging - EMS_SYS_LOGGING_VERBOSE, EMS_SYS_LOGGING_NONE, EMS_SYS_LOGGING_BASIC (see ems.h) -#define BOILER_DEFAULT_LOGGING EMS_SYS_LOGGING_NONE - -// shower settings for DEBUGGING only -#ifdef SHOWER_TEST -#undef SHOWER_PAUSE_TIME -#undef SHOWER_MIN_DURATION -#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 -#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 -} _Boiler_Status; - -typedef struct { - bool showerOn; - unsigned long timerStart; // ms - unsigned long timerPause; // ms - unsigned long duration; // ms - bool doingColdShot; // true if we've just sent a jolt of cold water -} _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"}, - {"h", "list supported EMS telegram type IDs"}, - {"M", "publish to MQTT"}, - {"Q", "print Tx Queue"}, - {"P", "toggle EMS Poll response on/off"}, - {"X", "toggle EMS Tx transmission on/off"}, - {"S", "toggle Shower timer on/off"}, - {"A", "toggle shower Alert on/off"}, - {"r [s]", "send raw telegram to EMS (s=XX XX XX...)"}, - {"b [xx]", "send boiler read request (xx=telegram type ID in hex)"}, - {"t [xx]", "send thermostat read request (xx=telegram type ID in hex)"}, - {"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"} - -}; - -// 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; - -// Times -const unsigned long POLL_TIMEOUT_ERR = 10000; // if no signal from boiler for last 10 seconds, assume its offline -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); - } -} - -// convert float to char -char * _float_to_char(char * a, float f, uint8_t precision = 1) { - long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; - - char * ret = a; - // check for 0x8000 (sensor missing) - if (f == EMS_VALUE_FLOAT_NOTSET) { - strcpy(ret, "?"); - } else { - long whole = (long)f; - itoa(whole, a, 10); - while (*a != '\0') - a++; - *a++ = '.'; - long decimal = abs((long)((f - whole) * p[precision])); - itoa(decimal, a, 10); - } - return ret; -} - -// convert bool to text -char * _bool_to_char(char * s, uint8_t value) { - if (value == EMS_VALUE_INT_ON) { - strcpy(s, "on"); - } else if (value == EMS_VALUE_INT_OFF) { - strcpy(s, "off"); - } else { - strcpy(s, "?"); - } - return s; -} - -// convert int to text value -char * _int_to_char(char * s, uint8_t value) { - if (value == EMS_VALUE_INT_NOTSET) { - strcpy(s, "?"); - } else { - itoa(value, s, 10); - } - return s; -} - -// takes a float value at prints it to debug log -void _renderFloatValue(const char * prefix, const char * postfix, float value) { - myDebug(" %s: ", prefix); - char s[20]; - myDebug("%s", _float_to_char(s, value)); - - if (postfix != NULL) { - myDebug(" %s", postfix); - } - - myDebug("\n"); -} - -// takes an int 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)); - - if (postfix != NULL) { - myDebug(" %s", postfix); - } - - myDebug("\n"); -} - -// 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)); -} - -// 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 "); - _EMS_SYS_LOGGING sysLog = ems_getLogging(); - if (sysLog == EMS_SYS_LOGGING_BASIC) { - myDebug("Basic"); - } else if (sysLog == EMS_SYS_LOGGING_VERBOSE) { - myDebug("Verbose"); - } else if (sysLog == EMS_SYS_LOGGING_THERMOSTAT) { - myDebug("Thermostat only"); - } else { - myDebug("None"); - } - - myDebug("\n # EMS type handlers: %d\n", ems_getEmsTypesCount()); - - myDebug(" Thermostat is %s, Poll is %s, Tx is %s, Shower Timer is %s, Shower Alert is %s\n", - (ems_getThermostatEnabled() ? "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"), - EMS_Sys_Status.emsRxPgks, - EMS_Sys_Status.emsTxPkgs, - EMS_Sys_Status.emxCrcErr); - - myDebug("\n%sBoiler stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); - - // 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")); - - // UBAParameterWW - _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); - _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); - _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); - _renderBoolValue("Warm Water 3-way valve", EMS_Boiler.wWHeat); - - // UBAMonitorFast - _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); - _renderFloatValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); - _renderFloatValue("Return temperature", "C", EMS_Boiler.retTemp); - _renderBoolValue("Gas", EMS_Boiler.burnGas); - _renderBoolValue("Boiler pump", EMS_Boiler.heatPmp); - _renderBoolValue("Fan", EMS_Boiler.fanWork); - _renderBoolValue("Ignition", EMS_Boiler.ignWork); - _renderBoolValue("Circulation pump", EMS_Boiler.wWCirc); - _renderIntValue("Burner selected max power", "%", EMS_Boiler.selBurnPow); - _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); - _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); - _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); - - // 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); - - // 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", - 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"); - } else if (EMS_Thermostat.mode == 1) { - myDebug("manual\n"); - } else if (EMS_Thermostat.mode == 2) { - myDebug("auto\n"); - } else { - myDebug("?\n"); - // myDebug("? (value is %d)\n", EMS_Thermostat.mode); - } - } - - // 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("\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 - - // Boiler values as one JSON object - StaticJsonBuffer<512> jsonBuffer; - char data[512]; - JsonObject & rootBoiler = jsonBuffer.createObject(); - - rootBoiler["wWSelTemp"] = _int_to_char(s, EMS_Boiler.wWSelTemp); - rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); - rootBoiler["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); - rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); - rootBoiler["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp); - rootBoiler["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp); - rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); - rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); - rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); - rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); - rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); - rootBoiler["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); - rootBoiler["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); - rootBoiler["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); - rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); - rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); - - size_t len = rootBoiler.measureLength(); - rootBoiler.printTo(data, len + 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++) { - 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"); - } - - // send values via MQTT - myESP.publish(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"); - - last_boilerActive = ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive); // remember last state - } - - // handle the thermostat values separately - if (ems_getThermostatEnabled()) { - // only send thermostat values if we actually have them - if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) - 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); - - // send mode 0=low, 1=manual, 2=auto - if (EMS_Thermostat.mode == 0) { - rootThermostat[TOPIC_THERMOSTAT_MODE] = "low"; - } else if (EMS_Thermostat.mode == 1) { - rootThermostat[TOPIC_THERMOSTAT_MODE] = "manual"; - } else { - rootThermostat[TOPIC_THERMOSTAT_MODE] = "auto"; - } - - size_t len = rootThermostat.measureLength(); - rootThermostat.printTo(data, len + 1); // form the json string - - // calculate new CRC - crc.reset(); - for (size_t i = 0; i < len - 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"); - } - - // send values via MQTT - myESP.publish(TOPIC_THERMOSTAT_DATA, data); - } - } -} - -// 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"); - } -} - -// 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"); - } -} - -// extra commands options for telnet debug window -void myDebugCallback() { - char * cmd = myESP.consoleGetLastCommand(); - uint8_t len = strlen(cmd); - bool b; - - // look for single letter commands - if (len == 1) { - switch (cmd[0]) { - case 's': - showInfo(); - break; - case 'P': // toggle Poll - b = !ems_getPoll(); - ems_setPoll(b); - break; - case 'X': // toggle Tx - b = !ems_getTxEnabled(); - ems_setTxEnabled(b); - break; - case 'M': - //myESP.logger(LOG_HA, "Force publish values"); - publishValues(true); - break; - case 'h': // show type handlers - ems_printAllTypes(); - 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"); - 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"); - break; - case 'Q': //print Tx Queue - ems_printTxQueue(); - break; - default: - myDebug("Unknown command. Use ? for help.\n"); - break; - } - return; - } - - // for commands with parameters, assume command is just one letter - 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); - 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) - ems_setWarmTapWaterActivated(true); - else if ((cmd[2] - '0') == 0) - ems_setWarmTapWaterActivated(false); - break; - case 'b': // boiler read command - ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_ID_BOILER); - break; - case 't': // thermostat command - ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_ID_THERMOSTAT); - break; - case 'r': // send raw data - ems_sendRawTelegram(&cmd[2]); - break; - case 'x': // experimental, not displayed! - myDebug("Calling experimental...\n"); - 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"); - ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); - publishValuesTimer.detach(); - systemCheckTimer.detach(); - regularUpdatesTimer.detach(); - scanThermostat_count = (uint8_t)strtol(&cmd[2], 0, 16); - scanThermostat.attach(SCANTHERMOSTAT_TIME, do_scanThermostat); - break; - default: - myDebug("Unknown command. Use ? for help.\n"); - 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; - } - - // 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); - } - 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; - } - 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; - } - set_showerAlert(); - return; - } - - // shower cold shot - if (strcmp(topic, TOPIC_SHOWER_COLDSHOT) == 0) { - _showerColdShotStart(); - return; - } - - // 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(); - } - } -} - -// 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) - 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 - } -} - -// Initialize the boiler settings -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; - Boiler_Shower.timerPause = 0; - Boiler_Shower.duration = 0; - Boiler_Shower.doingColdShot = false; - - // heartbeat only if verbose logging - ems_setLogging(BOILER_DEFAULT_LOGGING); - updateHeartbeat(); -} - -// call PublishValues without forcing, so using CRC to see if we really need to publish -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 - } -} - -// Thermostat scan -void do_scanThermostat() { - //myDebug("Scanning %d..\n", scanThermostat_count); - ems_doReadCommand(scanThermostat_count, EMS_ID_THERMOSTAT); - scanThermostat_count++; -} - -// do a healthcheck every now and then to see if we 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", - 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 - } -} - -// 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) - } -} - -// turn off hot water to send a shot of cold -void _showerColdShotStart() { - myDebugLog("Shower: doing a shot of cold"); - ems_setWarmTapWaterActivated(false); - Boiler_Shower.doingColdShot = true; - // start the timer for n seconds which will reset the water back to hot - showerColdShotStopTimer.attach(SHOWER_COLDSHOT_DURATION, _showerColdShotStop); -} - -// turn back on the hot water for the shower -void _showerColdShotStop() { - if (Boiler_Shower.doingColdShot) { - myDebugLog("Shower: finished shot of cold. hot water back on"); - ems_setWarmTapWaterActivated(true); - Boiler_Shower.doingColdShot = false; - showerColdShotStopTimer.detach(); - } -} - -/* - * Shower Logic - */ -void showerCheck() { - // if already in cold mode, ignore all this logic until we're out of the cold blast - if (!Boiler_Shower.doingColdShot) { - // is the hot water running? - if (EMS_Boiler.tapwaterActive) { - // if heater was previously off, start the timer - if (Boiler_Shower.timerStart == 0) { - // hot water just started... - Boiler_Shower.timerStart = timestamp; - Boiler_Shower.timerPause = 0; // remove any last pauses - 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 - myDebugLog("Shower: exceeded max shower time"); -#endif - _showerColdShotStart(); - } - } - } else { // hot water is off - // 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)); - - if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { - myDebug("Shower: finished with duration %s\n", s); - } - myESP.publish(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; - _showerColdShotStop(); // turn hot water back on in case its off - } - } - } -} - -// -// Main loop -// -void loop() { - connectionStatus = myESP.loop(); - 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(); - } - - // publish the values to MQTT, regardless if the values haven't changed - if (ems_getEmsRefreshed()) { - publishValues(true); - ems_setEmsRefreshed(false); - } - - // do shower logic if its enabled - if (Boiler_Status.shower_timer) { - showerCheck(); - } - - yield(); // yield to prevent watchdog from timing out -} diff --git a/src/ds18.cpp b/src/ds18.cpp new file mode 100644 index 000000000..a95c60441 --- /dev/null +++ b/src/ds18.cpp @@ -0,0 +1,221 @@ +/* + * Dallas support for external settings + * Copied from Espurna - Copyright (C) 2017-2018 by Xose Pérez + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP + * + * See ChangeLog.md for history + * See README.md for Acknowledgments + * + */ + +#include "ds18.h" + +std::vector _devices; + +DS18::DS18() { + _wire = NULL; + _count = 0; + _gpio = GPIO_NONE; +} + +DS18::~DS18() { + if (_wire) + delete _wire; +} + +// init +uint8_t DS18::setup(uint8_t gpio) { + uint8_t count; + + _gpio = gpio; + + // OneWire + if (_wire) + delete _wire; + _wire = new OneWire(_gpio); + + // Search devices + count = loadDevices(); + + // If no devices found check again pulling up the line + if (count == 0) { + pinMode(_gpio, INPUT_PULLUP); + count = loadDevices(); + } + + _count = count; + + return count; +} + +// scan every 2 seconds +void DS18::loop() { + static unsigned long last = 0; + if (millis() - last < DS18_READ_INTERVAL) + return; + last = millis(); + + // Every second we either start a conversion or read the scratchpad + static bool conversion = true; + if (conversion) { + // Start conversion + _wire->reset(); + _wire->skip(); + _wire->write(DS18_CMD_START_CONVERSION, DS18_PARASITE); + + } else { + // Read scratchpads + for (unsigned char index = 0; index < _devices.size(); index++) { + // Read scratchpad + if (_wire->reset() == 0) { + // Force a CRC check error + _devices[index].data[0] = _devices[index].data[0] + 1; + return; + } + + _wire->select(_devices[index].address); + _wire->write(DS18_CMD_READ_SCRATCHPAD); + + uint8_t data[DS18_DATA_SIZE]; + for (unsigned char i = 0; i < DS18_DATA_SIZE; i++) { + data[i] = _wire->read(); + } + + if (_wire->reset() != 1) { + // Force a CRC check error + _devices[index].data[0] = _devices[index].data[0] + 1; + return; + } + + memcpy(_devices[index].data, data, DS18_DATA_SIZE); + } + } + + conversion = !conversion; +} + +// return string of the device, with name and address +char * DS18::getDeviceString(char * buffer, unsigned char index) { + uint8_t size = 128; + if (index < _count) { + uint8_t * address = _devices[index].address; + + unsigned char chip_id = chip(index); + if (chip_id == DS18_CHIP_DS18S20) { + strlcpy(buffer, "DS18S20", size); + } else if (chip_id == DS18_CHIP_DS18B20) { + strlcpy(buffer, "DS18B20", size); + } else if (chip_id == DS18_CHIP_DS1822) { + strlcpy(buffer, "DS1822", size); + } else if (chip_id == DS18_CHIP_DS1825) { + strlcpy(buffer, "DS1825", size); + } else { + strlcpy(buffer, "Unknown", size); + } + + char a[30] = {0}; + snprintf(a, + sizeof(a), + "(%02X%02X%02X%02X%02X%02X%02X%02X) @ GPIO%d", + address[0], + address[1], + address[2], + address[3], + address[4], + address[5], + address[6], + address[7], + _gpio); + + strlcat(buffer, a, size); + } else { + strlcpy(buffer, "invalid", size); + } + + return buffer; +} + + +/* + * Read sensor values + * + * Registers: + byte 0: temperature LSB + byte 1: temperature MSB + byte 2: high alarm temp + byte 3: low alarm temp + byte 4: DS18S20: store for crc + DS18B20 & DS1822: configuration register + byte 5: internal use & crc + byte 6: DS18S20: COUNT_REMAIN + DS18B20 & DS1822: store for crc + byte 7: DS18S20: COUNT_PER_C + DS18B20 & DS1822: store for crc + byte 8: SCRATCHPAD_CRC +*/ +double DS18::getValue(unsigned char index) { + if (index >= _count) + return 0; + + uint8_t * data = _devices[index].data; + + if (OneWire::crc8(data, DS18_DATA_SIZE - 1) != data[DS18_DATA_SIZE - 1]) { + return 0; + } + + int16_t raw = (data[1] << 8) | data[0]; + if (chip(index) == DS18_CHIP_DS18S20) { + raw = raw << 3; // 9 bit resolution default + if (data[7] == 0x10) { + raw = (raw & 0xFFF0) + 12 - data[6]; // "count remain" gives full 12 bit resolution + } + } else { + byte cfg = (data[4] & 0x60); + if (cfg == 0x00) + raw = raw & ~7; // 9 bit res, 93.75 ms + else if (cfg == 0x20) + raw = raw & ~3; // 10 bit res, 187.5 ms + else if (cfg == 0x40) + raw = raw & ~1; // 11 bit res, 375 ms + // 12 bit res, 750 ms + } + + double value = (float)raw / 16.0; + if (value == DS18_DISCONNECTED) { + return 0; + } + + return value; +} + +// check for a supported DS chip version +bool DS18::validateID(unsigned char id) { + return (id == DS18_CHIP_DS18S20) || (id == DS18_CHIP_DS18B20) || (id == DS18_CHIP_DS1822) || (id == DS18_CHIP_DS1825); +} + +// return the type +unsigned char DS18::chip(unsigned char index) { + if (index < _count) + return _devices[index].address[0]; + return 0; +} + +// scan for DS sensors and load into the vector +uint8_t DS18::loadDevices() { + uint8_t address[8]; + _wire->reset(); + _wire->reset_search(); + while (_wire->search(address)) { + // Check CRC + if (_wire->crc8(address, 7) == address[7]) { + // Check ID + if (validateID(address[0])) { + ds_device_t device; + memcpy(device.address, address, 8); + _devices.push_back(device); + } + } + } + return (_devices.size()); +} diff --git a/src/ds18.h b/src/ds18.h new file mode 100644 index 000000000..d4dd9cfeb --- /dev/null +++ b/src/ds18.h @@ -0,0 +1,55 @@ +/* + * Dallas support for external temperature sensors + * Copyright (C) 2017-2018 by Xose Pérez + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP + * + * See ChangeLog.md for history + * See README.md for Acknowledgments + * + */ + +#pragma once + +#include +#include + +#define DS18_CHIP_DS18S20 0x10 +#define DS18_CHIP_DS1822 0x22 +#define DS18_CHIP_DS18B20 0x28 +#define DS18_CHIP_DS1825 0x3B + +#define DS18_DATA_SIZE 9 +#define DS18_PARASITE 1 +#define DS18_DISCONNECTED -127 + +#define GPIO_NONE 0x99 +#define DS18_READ_INTERVAL 2000 // Force sensor read & cache every 2 seconds + +#define DS18_CMD_START_CONVERSION 0x44 +#define DS18_CMD_READ_SCRATCHPAD 0xBE + +typedef struct { + uint8_t address[8]; + uint8_t data[DS18_DATA_SIZE]; +} ds_device_t; + +class DS18 { + public: + DS18(); + ~DS18(); + + uint8_t setup(uint8_t gpio); + void loop(); + char * getDeviceString(char * s, unsigned char index); + double getValue(unsigned char index); + + protected: + bool validateID(unsigned char id); + unsigned char chip(unsigned char index); + uint8_t loadDevices(); + + OneWire * _wire; + uint8_t _count; // # devices + uint8_t _gpio; // the sensor pin +}; diff --git a/src/ems-esp.ino b/src/ems-esp.ino new file mode 100644 index 000000000..1c1b733c0 --- /dev/null +++ b/src/ems-esp.ino @@ -0,0 +1,1211 @@ +/* + * EMS-ESP + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP + * + * See ChangeLog.md for history + * See README.md for Acknowledgments + */ + +// local libraries +#include "ds18.h" +#include "ems.h" +#include "ems_devices.h" +#include "emsuart.h" +#include "my_config.h" +#include "version.h" + +// Dallas external temp sensors +DS18 ds18; + +// shared libraries +#include + +// public libraries +#include // https://github.com/bblanchon/ArduinoJson +#include // https://github.com/bakercp/CRC32 + +// standard arduino libs +#include // https://github.com/esp8266/Arduino/tree/master/libraries/Ticker + +#define myDebug(...) myESP.myDebug(__VA_ARGS__) +#define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) + +// timers, all values are in seconds +#define PUBLISHVALUES_TIME 120 // every 2 minutes publish MQTT values +Ticker publishValuesTimer; + +#define SYSTEMCHECK_TIME 20 // every 20 seconds check if Boiler is online +Ticker systemCheckTimer; + +#define REGULARUPDATES_TIME 60 // every minute a call is made to fetch data from EMS devices manually +Ticker regularUpdatesTimer; + +#define LEDCHECK_TIME 500 // every 1/2 second blink the heartbeat LED +Ticker ledcheckTimer; + +// thermostat scan - for debugging +Ticker scanThermostat; +#define SCANTHERMOSTAT_TIME 1 +uint8_t scanThermostat_count = 0; + +Ticker showerColdShotStopTimer; + +// 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 +#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 + +typedef struct { + bool shower_timer; // true if we want to report back on shower times + bool shower_alert; // true if we want the alert of cold water + bool led_enabled; // LED on/off + bool test_mode; // test mode to stop automatic Tx on/off + + unsigned long timestamp; // for internal timings, via millis() + uint8_t dallas_sensors; // count of dallas sensors + uint8_t led_gpio; + uint8_t dallas_gpio; +} _EMSESP_Status; + +typedef struct { + bool showerOn; + unsigned long timerStart; // ms + unsigned long timerPause; // ms + unsigned long duration; // ms + bool doingColdShot; // true if we've just sent a jolt of cold water +} _EMSESP_Shower; + +command_t PROGMEM project_cmds[] = { + + {"set led ", "toggle status LED on/off"}, + {"set led_gpio ", "set the LED pin. Default is the onboard LED (D1=5)"}, + {"set dallas_gpio ", "set the pin for external Dallas temperature sensors (D5=14)"}, + {"set thermostat_type ", "set the thermostat type id (e.g. 10 for 0x10)"}, + {"set boiler_type ", "set the boiler type id (e.g. 8 for 0x08)"}, + {"set test_mode ", "test_mode turns off all automatic reads"}, + {"info", "show data captured on the EMS bus"}, + {"log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, + {"publish", "publish all values to MQTT"}, + {"types", "list supported EMS telegram type IDs"}, + {"queue", "show current Tx queue"}, + {"autodetect", "discover EMS devices and attempt to automatically set boiler and thermostat"}, + {"shower ", "toggle either timer or alert on/off"}, + {"send XX ...", "send raw telegram data as hex to EMS bus"}, + {"thermostat read ", "send read request to the thermostat"}, + {"thermostat temp ", "set current thermostat temperature"}, + {"thermostat mode ", "set mode (0=low/night, 1=manual/day, 2=auto)"}, + {"thermostat scan ", "do a read on all type IDs"}, + {"boiler read ", "send read request to boiler"}, + {"boiler wwtemp ", "set boiler warm water temperature"}, + {"boiler tapwater ", "set boiler warm tap water on/off"} + +}; + +// store for overall system status +_EMSESP_Status EMSESP_Status; +_EMSESP_Shower EMSESP_Shower; + +// logging messages with fixed strings +void myDebugLog(const char * s) { + if (ems_getLogging() >= EMS_SYS_LOGGING_BASIC) { + myDebug(s); + } +} + +// convert float to char +char * _float_to_char(char * a, float f, uint8_t precision = 2) { + long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; + + char * ret = a; + // check for 0x8000 (sensor missing) + if (f == EMS_VALUE_FLOAT_NOTSET) { + strlcpy(ret, "?", sizeof(ret)); + } else { + long whole = (long)f; + itoa(whole, a, 10); + while (*a != '\0') + a++; + *a++ = '.'; + long decimal = abs((long)((f - whole) * p[precision])); + itoa(decimal, a, 10); + } + return ret; +} + +// convert bool to text +char * _bool_to_char(char * s, uint8_t value) { + if (value == EMS_VALUE_INT_ON) { + strlcpy(s, "on", sizeof(s)); + } else if (value == EMS_VALUE_INT_OFF) { + strlcpy(s, "off", sizeof(s)); + } else { + strlcpy(s, "?", sizeof(s)); + } + return s; +} + +// convert int (single byte) to text value +char * _int_to_char(char * s, uint8_t value) { + if (value == EMS_VALUE_INT_NOTSET) { + strlcpy(s, "?", sizeof(s)); + } else { + itoa(value, s, 10); + } + return s; +} + +// takes a float value at prints it to debug log +void _renderFloatValue(const char * prefix, const char * postfix, float value) { + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + strlcat(buffer, _float_to_char(s, value), sizeof(buffer)); + + if (postfix != NULL) { + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); + } + myDebug(buffer); +} + +// takes an int (single byte) value at prints it to debug log +void _renderIntValue(const char * prefix, const char * postfix, uint8_t value) { + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + strlcat(buffer, _int_to_char(s, value), sizeof(buffer)); + + if (postfix != NULL) { + strlcat(buffer, " ", sizeof(buffer)); + strlcat(buffer, postfix, sizeof(buffer)); + } + myDebug(buffer); +} + +// takes an int value, converts to a fraction +void _renderIntfractionalValue(const char * prefix, const char * postfix, uint8_t value, uint8_t decimals) { + char buffer[200] = {0}; + char s[20] = {0}; + strlcpy(buffer, " ", sizeof(buffer)); + strlcat(buffer, prefix, sizeof(buffer)); + strlcat(buffer, ": ", sizeof(buffer)); + + if (value == EMS_VALUE_INT_NOTSET) { + strlcat(buffer, "?", sizeof(buffer)); + } else { + strlcat(buffer, _int_to_char(s, value / (decimals * 10)), sizeof(buffer)); + strlcat(buffer, ".", sizeof(buffer)); + strlcat(buffer, _int_to_char(s, value % (decimals * 10)), sizeof(buffer)); + } + + if (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)); + } + + 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) { + 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 System stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + _EMS_SYS_LOGGING sysLog = ems_getLogging(); + if (sysLog == EMS_SYS_LOGGING_BASIC) { + myDebug(" System logging set to Basic"); + } else if (sysLog == EMS_SYS_LOGGING_VERBOSE) { + myDebug(" System logging set to Verbose"); + } else if (sysLog == EMS_SYS_LOGGING_THERMOSTAT) { + myDebug(" System logging set to Thermostat only"); + } else { + myDebug(" System logging set to None"); + } + + myDebug(" LED is %s", EMSESP_Status.led_enabled ? "on" : "off"); + myDebug(" Test Mode is %s", EMSESP_Status.test_mode ? "on" : "off"); + + myDebug(" # connected Dallas temperature sensors=%d", EMSESP_Status.dallas_sensors); + + myDebug(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s", + (ems_getThermostatEnabled() ? "enabled" : "disabled"), + (ems_getBoilerEnabled() ? "enabled" : "disabled"), + ((EMSESP_Status.shower_timer) ? "enabled" : "disabled"), + ((EMSESP_Status.shower_alert) ? "enabled" : "disabled")); + + myDebug("\n%sEMS Bus Stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug(" Bus 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(""); + + myDebug("%sBoiler stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + + // version details + char buffer_type[64]; + myDebug(" Boiler type: %s", ems_getBoilerDescription(buffer_type)); + + // active stats + if (ems_getBusConnected()) { + myDebug(" Hot tap water is %s", (EMS_Boiler.tapwaterActive ? "running" : "off")); + myDebug(" Central Heating is %s", (EMS_Boiler.heatingActive ? "active" : "off")); + } + + // 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); + _renderIntfractionalValue("Warm Water current tap water 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 + _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); + _renderFloatValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); + _renderFloatValue("Return temperature", "C", EMS_Boiler.retTemp); + _renderBoolValue("Gas", EMS_Boiler.burnGas); + _renderBoolValue("Boiler pump", EMS_Boiler.heatPmp); + _renderBoolValue("Fan", EMS_Boiler.fanWork); + _renderBoolValue("Ignition", EMS_Boiler.ignWork); + _renderBoolValue("Circulation pump", EMS_Boiler.wWCirc); + _renderIntValue("Burner selected max power", "%", EMS_Boiler.selBurnPow); + _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); + _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); + _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); + myDebug(" Current System Service Code: %s", EMS_Boiler.serviceCodeChar); + + // UBAParametersMessage + _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); + _renderIntValue("Boiler circuit pump modulation max. power", "%", EMS_Boiler.pump_mod_max); + _renderIntValue("Boiler circuit pump modulation min. power", "%", EMS_Boiler.pump_mod_min); + + // UBAMonitorSlow + if (EMS_Boiler.extTemp != EMS_VALUE_FLOAT_NOTSET) { + _renderFloatValue("Outside temperature", "C", EMS_Boiler.extTemp); + } + _renderFloatValue("Boiler temperature", "C", EMS_Boiler.boilTemp); + _renderIntValue("Pump modulation", "%", EMS_Boiler.pumpMod); + _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("%sThermostat stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug(" Thermostat type: %s", ems_getThermostatDescription(buffer_type)); + _renderFloatValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp); + _renderFloatValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp); + if ((ems_getThermostatModel() != EMS_MODEL_EASY) && (ems_getThermostatModel() != EMS_MODEL_BOSCHEASY)) { + 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); + + if (EMS_Thermostat.mode == 0) { + myDebug(" Mode is set to low"); + } else if (EMS_Thermostat.mode == 1) { + myDebug(" Mode is set to manual"); + } else if (EMS_Thermostat.mode == 2) { + myDebug(" Mode is set to auto"); + } else { + myDebug(" Mode is set to ?"); + } + } + myDebug(""); // newline + } + + // Dallas + if (EMSESP_Status.dallas_sensors != 0) { + myDebug("%sExternal temperature sensors:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { + char s[80] = {0}; + snprintf(s, sizeof(s), "Sensor #%d", i + 1); + _renderFloatValue(s, "C", ds18.getValue(i)); + } + myDebug(""); // newline + } + + // show the Shower Info + if (EMSESP_Status.shower_timer) { + myDebug("%sShower stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug(" Shower is %s", (EMSESP_Shower.showerOn ? "running" : "off")); + } +} + +// send values via MQTT +// a json object is created for the boiler and one for the thermostat +// CRC check is done to see if there are changes in the values since the last send to avoid too much wifi traffic +void publishValues(bool force) { + char s[20] = {0}; // for formatting strings + StaticJsonDocument doc; + char data[MQTT_MAX_SIZE] = {0}; + CRC32 crc; + uint32_t fchecksum; + + static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off + static uint32_t previousBoilerPublishCRC = 0; // CRC check + static uint32_t previousThermostatPublishCRC = 0; // CRC check + + JsonObject rootBoiler = doc.to(); + + rootBoiler["wWSelTemp"] = _int_to_char(s, EMS_Boiler.wWSelTemp); + rootBoiler["selFlowTemp"] = _float_to_char(s, EMS_Boiler.selFlowTemp); + rootBoiler["outdoorTemp"] = _float_to_char(s, EMS_Boiler.extTemp); + rootBoiler["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); + rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); + rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); + rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); + rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); + rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); + rootBoiler["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); + rootBoiler["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); + rootBoiler["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); + rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); + rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); + rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + + serializeJson(doc, data, sizeof(data)); + + // calculate hash and send values if something has changed, to save unnecessary wifi traffic + for (size_t i = 0; i < measureJson(doc) - 1; i++) { + crc.update(data[i]); + } + fchecksum = crc.finalize(); + if ((previousBoilerPublishCRC != fchecksum) || force) { + previousBoilerPublishCRC = fchecksum; + myDebugLog("Publishing boiler data via MQTT"); + + // send values via MQTT + myESP.mqttPublish(TOPIC_BOILER_DATA, data); + } + + // see if the heating or hot tap water has changed, if so send + // last_boilerActive stores heating in bit 1 and tap water in bit 2 + if ((last_boilerActive != ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive)) || force) { + myDebugLog("Publishing hot water and heating states via MQTT"); + myESP.mqttPublish(TOPIC_BOILER_TAPWATER_ACTIVE, EMS_Boiler.tapwaterActive == 1 ? "1" : "0"); + myESP.mqttPublish(TOPIC_BOILER_HEATING_ACTIVE, EMS_Boiler.heatingActive == 1 ? "1" : "0"); + + last_boilerActive = ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive); // remember last state + } + + // handle the thermostat values separately + if (ems_getThermostatEnabled()) { + // only send thermostat values if we actually have them + if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) + return; + + // build new json object + doc.clear(); + JsonObject rootThermostat = doc.to(); + + rootThermostat[THERMOSTAT_CURRTEMP] = _float_to_char(s, EMS_Thermostat.curr_roomTemp); + rootThermostat[THERMOSTAT_SELTEMP] = _float_to_char(s, EMS_Thermostat.setpoint_roomTemp); + + // RC20 has different mode settings + if (ems_getThermostatModel() == EMS_MODEL_RC20) { + if (EMS_Thermostat.mode == 0) { + rootThermostat[THERMOSTAT_MODE] = "low"; + } else if (EMS_Thermostat.mode == 1) { + rootThermostat[THERMOSTAT_MODE] = "manual"; + } else { + rootThermostat[THERMOSTAT_MODE] = "auto"; + } + } else { + if (EMS_Thermostat.mode == 0) { + rootThermostat[THERMOSTAT_MODE] = "night"; + } else if (EMS_Thermostat.mode == 1) { + rootThermostat[THERMOSTAT_MODE] = "day"; + } else { + rootThermostat[THERMOSTAT_MODE] = "auto"; + } + } + + data[0] = '\0'; // reset data for next package + serializeJson(doc, data, sizeof(data)); + + // calculate new CRC + crc.reset(); + for (size_t i = 0; i < measureJson(doc) - 1; i++) { + crc.update(data[i]); + } + uint32_t checksum = crc.finalize(); + if ((previousThermostatPublishCRC != checksum) || force) { + previousThermostatPublishCRC = checksum; + myDebugLog("Publishing thermostat data via MQTT"); + + // send values via MQTT + myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); + } + } +} + +// sets the shower timer on/off +void set_showerTimer() { + if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + myDebug("Shower timer has been set to %s", EMSESP_Status.shower_timer ? "enabled" : "disabled"); + } +} + +// sets the shower alert on/off +void set_showerAlert() { + if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + myDebug("Shower alert has been set to %s", EMSESP_Status.shower_alert ? "enabled" : "disabled"); + } +} + +// used to read the next string from an input buffer and convert to an 8 bit int +uint8_t _readIntNumber() { + char * numTextPtr = strtok(NULL, ", \n"); + if (numTextPtr == nullptr) { + return 0; + } + return atoi(numTextPtr); +} + +// used to read the next string from an input buffer and convert to a double +float _readFloatNumber() { + char * numTextPtr = strtok(NULL, ", \n"); + if (numTextPtr == nullptr) { + return 0; + } + return atof(numTextPtr); +} + +// used to read the next string from an input buffer as a hex value and convert to an 8 bit int +uint8_t _readHexNumber() { + char * numTextPtr = strtok(NULL, ", \n"); + if (numTextPtr == nullptr) { + return 0; + } + return (uint8_t)strtol(numTextPtr, 0, 16); +} + +// used to read the next string from an input buffer +char * _readWord() { + char * word = strtok(NULL, ", \n"); + return word; +} + +// initiate a force scan by sending type read requests from 0 to FF to the thermostat +// used to analyze responses for debugging +void startThermostatScan(uint8_t start) { + ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); + publishValuesTimer.detach(); + systemCheckTimer.detach(); + regularUpdatesTimer.detach(); + scanThermostat_count = start; + myDebug("Starting a deep message scan on thermostat"); + scanThermostat.attach(SCANTHERMOSTAT_TIME, do_scanThermostat); +} + +// callback for loading/saving settings to the file system (SPIFFS) +bool FSCallback(MYESP_FSACTION action, const JsonObject json) { + if (action == MYESP_FSACTION_LOAD) { + // led + if (!(EMSESP_Status.led_enabled = json["led"])) { + EMSESP_Status.led_enabled = LED_BUILTIN; // default value + } + + // led_gpio + if (!(EMSESP_Status.led_gpio = json["led_gpio"])) { + EMSESP_Status.led_gpio = EMSESP_LED_GPIO; // default value + } + + // dallas_gpio + if (!(EMSESP_Status.dallas_gpio = json["dallas_gpio"])) { + EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; // default value + } + + // thermostat_type + if (!(EMS_Thermostat.type_id = json["thermostat_type"])) { + EMS_Thermostat.type_id = EMSESP_THERMOSTAT_TYPE; // set default + } + + // boiler_type + if (!(EMS_Boiler.type_id = json["boiler_type"])) { + EMS_Boiler.type_id = EMSESP_BOILER_TYPE; // set default + } + + // test mode + if (!(EMSESP_Status.test_mode = json["test_mode"])) { + EMSESP_Status.test_mode = false; // default value + } + + return false; // always save the settings + } + + if (action == MYESP_FSACTION_SAVE) { + json["led"] = EMSESP_Status.led_enabled; + json["led_gpio"] = EMSESP_Status.led_gpio; + json["dallas_gpio"] = EMSESP_Status.dallas_gpio; + json["thermostat_type"] = EMS_Thermostat.type_id; + json["boiler_type"] = EMS_Boiler.type_id; + json["test_mode"] = EMSESP_Status.test_mode; + + return true; + } + + return false; +} + +// callback for custom settings when showing Stored Settings +// wc is number of arguments after the 'set' command +// returns true if the setting was recognized and changed +bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) { + bool ok = false; + + if (action == MYESP_FSACTION_SET) { + // led + if ((strcmp(setting, "led") == 0) && (wc == 2)) { + if (strcmp(value, "on") == 0) { + EMSESP_Status.led_enabled = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + EMSESP_Status.led_enabled = false; + ok = true; + // let's make sure LED is really off + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off + } + } + + // test mode + if ((strcmp(setting, "test_mode") == 0) && (wc == 2)) { + if (strcmp(value, "on") == 0) { + EMSESP_Status.test_mode = true; + ok = true; + myDebug("* Reboot to go into test mode."); + } else if (strcmp(value, "off") == 0) { + EMSESP_Status.test_mode = false; + ok = true; + } + } + + // led_gpio + if ((strcmp(setting, "led_gpio") == 0) && (wc == 2)) { + EMSESP_Status.led_gpio = atoi(value); + // reset pin + pinMode(EMSESP_Status.led_gpio, OUTPUT); + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off + ok = true; + } + + // dallas_gpio + if ((strcmp(setting, "dallas_gpio") == 0) && (wc == 2)) { + EMSESP_Status.dallas_gpio = atoi(value); + ok = true; + } + + // thermostat_type + if (strcmp(setting, "thermostat_type") == 0) { + EMS_Thermostat.type_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); + ok = true; + } + + // boiler_type + if (strcmp(setting, "boiler_type") == 0) { + EMS_Boiler.type_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); + ok = true; + } + } + + if (action == MYESP_FSACTION_LIST) { + myDebug(" test_mode=%s", EMSESP_Status.test_mode ? "on" : "off"); + myDebug(" led=%s", EMSESP_Status.led_enabled ? "on" : "off"); + myDebug(" led_gpio=%d", EMSESP_Status.led_gpio); + myDebug(" dallas_gpio=%d", EMSESP_Status.dallas_gpio); + + if (EMS_Thermostat.type_id == EMS_ID_NONE) { + myDebug(" thermostat_type="); + + } else { + myDebug(" thermostat_type=%02X", EMS_Thermostat.type_id); + } + + if (EMS_Boiler.type_id == EMS_ID_NONE) { + myDebug(" boiler_type="); + + } else { + myDebug(" boiler_type=%02X", EMS_Boiler.type_id); + } + } + + return ok; +} + +// call back when a telnet client connects or disconnects +// we set the logging here +void TelnetCallback(uint8_t event) { + if (event == TELNET_EVENT_CONNECT) { + ems_setLogging(EMS_SYS_LOGGING_DEFAULT); + } else if (event == TELNET_EVENT_DISCONNECT) { + ems_setLogging(EMS_SYS_LOGGING_NONE); + } +} + +// extra commands options for telnet debug window +// wc is the word count, i.e. number of arguments. Everything is in lower case. +void TelnetCommandCallback(uint8_t wc, const char * commandLine) { + bool ok = false; + // get first command argument + char * first_cmd = strtok((char *)commandLine, ", \n"); + + if (strcmp(first_cmd, "info") == 0) { + showInfo(); + ok = true; + } + + if (strcmp(first_cmd, "publish") == 0) { + publishValues(true); + ok = true; + } + + if (strcmp(first_cmd, "types") == 0) { + ems_printAllTypes(); + ok = true; + } + + if (strcmp(first_cmd, "queue") == 0) { + ems_printTxQueue(); + ok = true; + } + + if (strcmp(first_cmd, "autodetect") == 0) { + ems_scanDevices(); + ok = true; + } + + // shower settings + if (strcmp(first_cmd, "shower") == 0) { + if (wc == 2) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "timer") == 0) { + EMSESP_Status.shower_timer = !EMSESP_Status.shower_timer; + myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0"); + ok = true; + } else if (strcmp(second_cmd, "alert") == 0) { + EMSESP_Status.shower_alert = !EMSESP_Status.shower_alert; + myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0"); + ok = true; + } + } + } + + // logging + if (strcmp(first_cmd, "log") == 0) { + if (wc == 2) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "v") == 0) { + ems_setLogging(EMS_SYS_LOGGING_VERBOSE); + ok = true; + } else if (strcmp(second_cmd, "b") == 0) { + ems_setLogging(EMS_SYS_LOGGING_BASIC); + ok = true; + } else if (strcmp(second_cmd, "t") == 0) { + ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); + ok = true; + } else if (strcmp(second_cmd, "r") == 0) { + ems_setLogging(EMS_SYS_LOGGING_RAW); + ok = true; + } else if (strcmp(second_cmd, "n") == 0) { + ems_setLogging(EMS_SYS_LOGGING_NONE); + ok = true; + } + } + } + + // thermostat commands + if (strcmp(first_cmd, "thermostat") == 0) { + if (wc == 3) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "temp") == 0) { + ems_setThermostatTemp(_readFloatNumber()); + ok = true; + } else if (strcmp(second_cmd, "mode") == 0) { + ems_setThermostatMode(_readIntNumber()); + ok = true; + } else if (strcmp(second_cmd, "read") == 0) { + ems_doReadCommand(_readHexNumber(), EMS_Thermostat.type_id); + ok = true; + } else if (strcmp(second_cmd, "scan") == 0) { + startThermostatScan(_readIntNumber()); + ok = true; + } + } + } + + // boiler commands + if (strcmp(first_cmd, "boiler") == 0) { + if (wc == 3) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "wwtemp") == 0) { + ems_setWarmWaterTemp(_readIntNumber()); + ok = true; + } else if (strcmp(second_cmd, "read") == 0) { + ems_doReadCommand(_readHexNumber(), EMS_Boiler.type_id); + ok = true; + } else if (strcmp(second_cmd, "tapwater") == 0) { + char * third_cmd = _readWord(); + if (strcmp(third_cmd, "on") == 0) { + ems_setWarmTapWaterActivated(true); + ok = true; + } else if (strcmp(third_cmd, "off") == 0) { + ems_setWarmTapWaterActivated(false); + ok = true; + } + } + } + } + + // send raw + if (strcmp(first_cmd, "send") == 0) { + ems_sendRawTelegram((char *)&commandLine[5]); + ok = true; + } + + // check for invalid command + if (!ok) { + myDebug("Unknown command. Use ? for help."); + } +} + +// OTA callback when the OTA process starts +// so we can disable the EMS to avoid any noise +void OTACallback() { + emsuart_stop(); +} + +// MQTT Callback to handle incoming/outgoing changes +void MQTTCallback(unsigned int type, const char * topic, const char * message) { + // we're connected. lets subscribe to some topics + if (type == MQTT_CONNECT_EVENT) { + myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_TEMP); + myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE); + myESP.mqttSubscribe(TOPIC_BOILER_WWACTIVATED); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); + myESP.mqttSubscribe(TOPIC_SHOWER_TIMER); + myESP.mqttSubscribe(TOPIC_SHOWER_ALERT); + myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT); + + // subscribe to a start message and send the first publish + myESP.mqttSubscribe(MQTT_TOPIC_START); + myESP.mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD); + + // publish the status of the Shower parameters + myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0"); + myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0"); + } + + // handle incoming MQTT publish events + if (type == MQTT_MESSAGE_EVENT) { + // handle response from a start message + // for example with HA it sends the system time from the server + if (strcmp(topic, MQTT_TOPIC_START) == 0) { + myDebug("Received boottime: %s", message); + myESP.setBoottime(message); + } + + // 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 immediately + } + + // 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, "day") == 0) { + ems_setThermostatMode(1); + } else if (strcmp((char *)message, "night") == 0) { + ems_setThermostatMode(0); + } + } + + // wwActivated + if (strcmp(topic, TOPIC_BOILER_WWACTIVATED) == 0) { + if (message[0] == '1') { + ems_setWarmWaterActivated(true); + } else if (message[0] == '0') { + ems_setWarmWaterActivated(false); + } + } + + // boiler wwtemp changes + if (strcmp(topic, TOPIC_BOILER_CMD_WWTEMP) == 0) { + float f = strtof((char *)message, 0); + char s[10] = {0}; + myDebug("MQTT topic: boiler warm water temperature value %s", _float_to_char(s, f)); + ems_setWarmWaterTemp(f); + publishValues(true); // publish back immediately + } + + // shower timer + if (strcmp(topic, TOPIC_SHOWER_TIMER) == 0) { + if (message[0] == '1') { + EMSESP_Status.shower_timer = true; + } else if (message[0] == '0') { + EMSESP_Status.shower_timer = false; + } + set_showerTimer(); + } + + // shower alert + if (strcmp(topic, TOPIC_SHOWER_ALERT) == 0) { + if (message[0] == '1') { + EMSESP_Status.shower_alert = true; + } else if (message[0] == '0') { + EMSESP_Status.shower_alert = false; + } + set_showerAlert(); + } + + // shower cold shot + if (strcmp(topic, TOPIC_SHOWER_COLDSHOT) == 0) { + _showerColdShotStart(); + } + } +} + +// Init callback, which is used to set functions and call methods after a wifi connection has been established +void WIFICallback() { + // This is where we enable the UART service to scan the incoming serial Tx/Rx bus signals + // This is done after we have a WiFi signal to avoid any resource conflicts + + if (myESP.getUseSerial()) { + myDebug("Warning! EMS bus disabled when in Serial mode. Use 'set serial off' to enable."); + } else { + emsuart_init(); + myDebug("[UART] Opened Rx/Tx connection"); + // go and find the boiler and thermostat types + ems_discoverModels(); + } +} + +// Initialize the boiler settings and shower settings +void initEMSESP() { + // general settings + EMSESP_Status.shower_timer = BOILER_SHOWER_TIMER; + EMSESP_Status.shower_alert = BOILER_SHOWER_ALERT; + EMSESP_Status.led_enabled = true; // LED is on by default + EMSESP_Status.test_mode = false; + + EMSESP_Status.timestamp = millis(); + EMSESP_Status.dallas_sensors = 0; + + EMSESP_Status.led_gpio = EMSESP_LED_GPIO; + EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; + + // shower settings + EMSESP_Shower.timerStart = 0; + EMSESP_Shower.timerPause = 0; + EMSESP_Shower.duration = 0; + EMSESP_Shower.doingColdShot = false; +} + +// call PublishValues without forcing, so using CRC to see if we really need to publish +void do_publishValues() { + // don't publish if we're not connected to the EMS bus + if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + publishValues(false); + } +} + +// callback to light up the LED, called via Ticker every second +// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off +void do_ledcheck() { + if (EMSESP_Status.led_enabled) { + if (ems_getBusConnected()) { + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off + } else { + int state = digitalRead(EMSESP_Status.led_gpio); + digitalWrite(EMSESP_Status.led_gpio, !state); + } + } +} + +// Thermostat scan +void do_scanThermostat() { + if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebug("> Scanning thermostat message type #0x%02X..", scanThermostat_count); + ems_doReadCommand(scanThermostat_count, EMS_Thermostat.type_id); + scanThermostat_count++; + } +} + +// do a system health check every now and then to see if we all connections +void do_systemCheck() { + if ((!ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebug("Error! Unable to read from EMS bus. Retrying in %d seconds...", SYSTEMCHECK_TIME); + } +} + +// force calls to get data from EMS for the types that aren't sent as broadcasts +// only if we have a EMS connection +void do_regularUpdates() { + if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebugLog("Calling scheduled data refresh from EMS devices.."); + ems_getThermostatValues(); + ems_getBoilerValues(); + } +} + +// turn off hot water to send a shot of cold +void _showerColdShotStart() { + if (EMSESP_Status.shower_alert) { + myDebugLog("[Shower] doing a shot of cold water"); + ems_setWarmTapWaterActivated(false); + EMSESP_Shower.doingColdShot = true; + // start the timer for n seconds which will reset the water back to hot + showerColdShotStopTimer.attach(SHOWER_COLDSHOT_DURATION, _showerColdShotStop); + } +} + +// turn back on the hot water for the shower +void _showerColdShotStop() { + if (EMSESP_Shower.doingColdShot) { + myDebugLog("[Shower] finished shot of cold. hot water back on"); + ems_setWarmTapWaterActivated(true); + EMSESP_Shower.doingColdShot = false; + showerColdShotStopTimer.detach(); // disable the timer + } +} + +/* + * Shower Logic + */ +void showerCheck() { + // if already in cold mode, ignore all this logic until we're out of the cold blast + if (!EMSESP_Shower.doingColdShot) { + // is the hot water running? + if (EMS_Boiler.tapwaterActive) { + // if heater was previously off, start the timer + if (EMSESP_Shower.timerStart == 0) { + // hot water just started... + EMSESP_Shower.timerStart = EMSESP_Status.timestamp; + EMSESP_Shower.timerPause = 0; // remove any last pauses + EMSESP_Shower.doingColdShot = false; + EMSESP_Shower.duration = 0; + EMSESP_Shower.showerOn = false; + } 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 (!EMSESP_Shower.showerOn && (EMSESP_Status.timestamp - EMSESP_Shower.timerStart) > SHOWER_MIN_DURATION) { + EMSESP_Shower.showerOn = true; + myDebugLog("[Shower] hot water still running, starting shower timer"); + } + // check if the shower has been on too long + else if ((((EMSESP_Status.timestamp - EMSESP_Shower.timerStart) > SHOWER_MAX_DURATION) && !EMSESP_Shower.doingColdShot) + && EMSESP_Status.shower_alert) { + myDebugLog("[Shower] exceeded max shower time"); + _showerColdShotStart(); + } + } + } else { // hot water is off + // if it just turned off, record the time as it could be a short pause + if ((EMSESP_Shower.timerStart != 0) && (EMSESP_Shower.timerPause == 0)) { + EMSESP_Shower.timerPause = EMSESP_Status.timestamp; + } + + // if shower has been off for longer than the wait time + if ((EMSESP_Shower.timerPause != 0) && ((EMSESP_Status.timestamp - EMSESP_Shower.timerPause) > SHOWER_PAUSE_TIME)) { + // 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 ((EMSESP_Shower.timerPause - EMSESP_Shower.timerStart) > SHOWER_OFFSET_TIME) { + EMSESP_Shower.duration = (EMSESP_Shower.timerPause - EMSESP_Shower.timerStart - SHOWER_OFFSET_TIME); + if (EMSESP_Shower.duration > SHOWER_MIN_DURATION) { + char s[50] = {0}; + char buffer[16] = {0}; + strlcpy(s, itoa((uint8_t)((EMSESP_Shower.duration / (1000 * 60)) % 60), buffer, 10), sizeof(s)); + strlcat(s, " minutes and ", sizeof(s)); + strlcat(s, itoa((uint8_t)((EMSESP_Shower.duration / 1000) % 60), buffer, 10), sizeof(s)); + strlcat(s, " seconds", sizeof(s)); + if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + myDebug("[Shower] finished with duration %s", s); + } + myESP.mqttPublish(TOPIC_SHOWERTIME, s); // publish to MQTT + } + } + + // reset everything + EMSESP_Shower.timerStart = 0; + EMSESP_Shower.timerPause = 0; + EMSESP_Shower.showerOn = false; + _showerColdShotStop(); // turn hot water back on in case its off + } + } + } +} + +// +// SETUP +// +void setup() { + // init our own parameters + initEMSESP(); + + // call ems.cpp's init function to set all the internal params + ems_init(); + + systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if Boiler is online + + // set up myESP for Wifi, MQTT, MDNS and Telnet + myESP.setTelnet(project_cmds, ArraySize(project_cmds), TelnetCommandCallback, TelnetCallback); // set up Telnet commands +#ifdef WIFI_SSID + myESP.setWIFI(WIFI_SSID, WIFI_PASSWORD, WIFICallback); +#else + myESP.setWIFI(NULL, NULL, WIFICallback); // pull the wifi settings from the SPIFFS stored settings +#endif + + // MQTT host, username and password taken from the SPIFFS settings + myESP.setMQTT(NULL, + NULL, + NULL, + MQTT_BASE, + MQTT_KEEPALIVE, + MQTT_QOS, + MQTT_RETAIN, + MQTT_WILL_TOPIC, + MQTT_WILL_ONLINE_PAYLOAD, + MQTT_WILL_OFFLINE_PAYLOAD, + MQTTCallback); + + // OTA callback which is called when OTA is starting + myESP.setOTA(OTACallback); + + // custom settings in SPIFFS + myESP.setSettings(FSCallback, SettingsCallback); + + // start up all the services + myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION); + + // enable regular checks if not in test mode + if (!EMSESP_Status.test_mode) { + publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post MQTT values + regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS + } + + // set pin for LED + if (EMSESP_Status.led_gpio != EMS_VALUE_INT_NOTSET) { + pinMode(EMSESP_Status.led_gpio, OUTPUT); + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off + ledcheckTimer.attach_ms(LEDCHECK_TIME, do_ledcheck); // blink heartbeat LED + } + + // check for Dallas sensors + EMSESP_Status.dallas_sensors = ds18.setup(EMSESP_Status.dallas_gpio); // returns #sensors +} + +// +// Main loop +// +void loop() { + EMSESP_Status.timestamp = millis(); + + // the main loop + myESP.loop(); + + // check Dallas sensors + if (EMSESP_Status.dallas_sensors != 0) { + ds18.loop(); + } + + // publish the values to MQTT, only if the values have changed + // although we don't want to publish when doing a deep scan of the thermostat + if (ems_getEmsRefreshed() && (scanThermostat_count == 0) && (!EMSESP_Status.test_mode)) { + publishValues(false); + ems_setEmsRefreshed(false); // reset + } + + // do shower logic, if enabled + if (EMSESP_Status.shower_timer) { + showerCheck(); + } +} diff --git a/src/ems.cpp b/src/ems.cpp index 05cb30c2e..76b263e3a 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -1,166 +1,192 @@ /** * ems.cpp - * handles all the processing of the EMS messages - * Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler + * + * Handles all the processing of the EMS messages + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP */ #include "ems.h" +#include "ems_devices.h" #include "emsuart.h" - #include #include // https://github.com/rlogiacco/CircularBuffer -#include +#include +#include // std::list -// 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 -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); -// Thermostat +// generic +void _process_Version(uint8_t type, uint8_t * data, uint8_t length); -// Common -void _process_Version(uint8_t * data, uint8_t length); -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); +// Boiler and Buderus devices +void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length); +void _process_UBAMonitorSlow(uint8_t type, uint8_t * data, uint8_t length); +void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length); +void _process_UBATotalUptimeMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_SetPoints(uint8_t type, uint8_t * data, uint8_t length); + +// Common for most thermostats +void _process_RCTime(uint8_t type, uint8_t * data, uint8_t length); +void _process_RCOutdoorTempMessage(uint8_t type, uint8_t * data, uint8_t length); + +// RC10 +void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length); // RC20 -void _process_RC20Set(uint8_t * data, uint8_t length); -void _process_RC20StatusMessage(uint8_t * data, uint8_t length); +void _process_RC20Set(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length); // RC30 -void _process_RC30Set(uint8_t * data, uint8_t length); -void _process_RC30StatusMessage(uint8_t * data, uint8_t length); +void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length); + +// RC35 +void _process_RC35Set(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length); // Easy -void _process_EasyStatusMessage(uint8_t * data, uint8_t length); +void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length); -const _Thermostat_Types Thermostat_Types[] = { +/* + * Recognized EMS types and the functions they call to process the telegrams + * Format: MODEL ID, TYPE ID, Description, function + */ +const _EMS_Type EMS_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)"} - -}; -uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types - -const _EMS_Types 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}, + {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", _process_UBAParametersMessage}, + {EMS_MODEL_UBA, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, - // 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}, + // RC10 + {EMS_MODEL_RC10, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_MODEL_RC10, EMS_TYPE_RC10Set, "RC10Set", _process_RC10Set}, + {EMS_MODEL_RC10, EMS_TYPE_RC10StatusMessage, "RC10StatusMessage", _process_RC10StatusMessage}, - // RC20 - {EMS_ID_THERMOSTAT, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, - {EMS_ID_THERMOSTAT, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, + // RC20 and RC20F + {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_RC20F, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + {EMS_MODEL_RC20F, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_MODEL_RC20F, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, + {EMS_MODEL_RC20F, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, // 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}, + + // RC35 + {EMS_MODEL_RC35, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + {EMS_MODEL_RC35, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_MODEL_RC35, EMS_TYPE_RC35Set, "RC35Set", _process_RC35Set}, + {EMS_MODEL_RC35, EMS_TYPE_RC35StatusMessage, "RC35StatusMessage", _process_RC35StatusMessage}, + + // ES73 + {EMS_MODEL_ES73, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, + {EMS_MODEL_ES73, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, + {EMS_MODEL_ES73, EMS_TYPE_RC35Set, "RC35Set", _process_RC35Set}, + {EMS_MODEL_ES73, EMS_TYPE_RC35StatusMessage, "RC35StatusMessage", _process_RC35StatusMessage}, // Easy - {EMS_ID_THERMOSTAT, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage} + {EMS_MODEL_EASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, + {EMS_MODEL_BOSCHEASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, + }; -uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types -// reserve space for the data we collect from the Boiler and Thermostat +// calculate sizes of arrays +uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types +uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of models +uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types + +// these structs contain the data we store from the Boiler and Thermostat _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 - -uint8_t emsRxRetryCount; // used for retries when sending failed +const uint8_t TX_WRITE_TIMEOUT_COUNT = 2; // 3 retries before timeout +const unsigned long EMS_BUS_TIMEOUT = 15000; // timeout in ms before recognizing the ems bus is offline (15 seconds) +const unsigned long EMS_POLL_TIMEOUT = 5000; // timeout in ms before recognizing the ems bus is offline (5 seconds) // 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.emsLogging = EMS_SYS_LOGGING_NONE; // Verbose logging is off + EMS_Sys_Status.emsRxPgks = 0; + EMS_Sys_Status.emsTxPkgs = 0; + EMS_Sys_Status.emxCrcErr = 0; + EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; + EMS_Sys_Status.emsRefreshed = false; + EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled + EMS_Sys_Status.emsBusConnected = false; + EMS_Sys_Status.emsRxTimestamp = 0; + EMS_Sys_Status.emsTxCapable = false; + EMS_Sys_Status.emsPollTimestamp = 0; + EMS_Sys_Status.txRetryCount = 0; // 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.day_mode = 255; // dummy value + + EMS_Thermostat.type_id = EMS_ID_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 @@ -176,51 +202,58 @@ void ems_init() { 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 + strlcpy(EMS_Boiler.serviceCodeChar, "??", sizeof(EMS_Boiler.serviceCodeChar)); // 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 + + // UBAParametersMessage + EMS_Boiler.heating_temp = EMS_VALUE_INT_NOTSET; // Heating temperature setting on the boiler + EMS_Boiler.pump_mod_max = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation max. power + EMS_Boiler.pump_mod_min = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation min. power + + // calculated values EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off EMS_Boiler.heatingActive = EMS_VALUE_INT_NOTSET; // Central heating is on/off + + // set boiler type + EMS_Boiler.product_id = 0; + strlcpy(EMS_Boiler.version, "not set", sizeof(EMS_Boiler.version)); + + // set thermostat model + EMS_Thermostat.model_id = EMS_MODEL_NONE; + EMS_Thermostat.product_id = 0; + strlcpy(EMS_Thermostat.version, "not set", sizeof(EMS_Thermostat.version)); + + // default logging is none + ems_setLogging(EMS_SYS_LOGGING_DEFAULT); } // 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"); -} - -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,41 +262,49 @@ void ems_setEmsRefreshed(bool b) { EMS_Sys_Status.emsRefreshed = b; } -bool ems_getThermostatEnabled() { - return EMS_Sys_Status.emsThermostatEnabled; +bool ems_getBoilerEnabled() { + return (EMS_Boiler.type_id != EMS_ID_NONE); } -void ems_setThermostatEnabled(bool b) { - EMS_Sys_Status.emsThermostatEnabled = b; - myDebug("Thermostat is set to %s\n", EMS_Sys_Status.emsThermostatEnabled ? "enabled" : "disabled"); +bool ems_getThermostatEnabled() { + return (EMS_Thermostat.type_id != EMS_ID_NONE); +} + +uint8_t ems_getThermostatModel() { + return (EMS_Thermostat.model_id); +} + +bool ems_getTxCapable() { + if ((millis() - EMS_Sys_Status.emsPollTimestamp) > EMS_POLL_TIMEOUT) { + EMS_Sys_Status.emsTxCapable = false; + } + return EMS_Sys_Status.emsTxCapable; +} + +bool ems_getBusConnected() { + if ((millis() - EMS_Sys_Status.emsRxTimestamp) > EMS_BUS_TIMEOUT) { + EMS_Sys_Status.emsBusConnected = false; + } + return EMS_Sys_Status.emsBusConnected; } _EMS_SYS_LOGGING ems_getLogging() { return EMS_Sys_Status.emsLogging; } -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 set to None"); } else if (loglevel == EMS_SYS_LOGGING_BASIC) { - myDebug("Basic\n"); + myDebug("System Logging set to Basic"); } else if (loglevel == EMS_SYS_LOGGING_VERBOSE) { - myDebug("Verbose\n"); + myDebug("System Logging set to Verbose"); } else if (loglevel == EMS_SYS_LOGGING_THERMOSTAT) { - myDebug("Thermostat only\n"); + myDebug("System Logging set to Thermostat only"); } else if (loglevel == EMS_SYS_LOGGING_RAW) { - myDebug("Raw mode\n"); + myDebug("System Logging set to Raw mode"); } } } @@ -271,6 +312,7 @@ void ems_setLogging(_EMS_SYS_LOGGING loglevel) { /** * Calculate CRC checksum using lookup table for speed * len is length of data in bytes (including the CRC byte at end) + * So its the complete telegram with the header */ uint8_t _crcCalculator(uint8_t * data, uint8_t len) { uint8_t crc = 0; @@ -284,7 +326,7 @@ uint8_t _crcCalculator(uint8_t * data, uint8_t len) { return crc; } -/** +/** * function to turn a telegram int (2 bytes) to a float. The source is *10 * negative values are stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c) */ @@ -301,13 +343,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 +371,69 @@ 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) { + 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,34 +452,19 @@ 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 return; } - // 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", - ((EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) ? "write" : "read"), - EMS_TxTelegram.dest & 0x7F); - EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; // finished sending - EMS_TxQueue.shift(); // remove from queue + // if there is no destination, also delete it from the queue + if (EMS_TxTelegram.dest == EMS_ID_NONE) { + EMS_TxQueue.shift(); // remove from queue return; } - // if 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 EMS_TxTelegram.data[0] = EMS_ID_ME; // src // dest @@ -413,18 +484,19 @@ void _ems_sendTelegram() { // for writing its the value we want to write EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; } - // finally calculate CRC and add it - EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); + // finally calculate CRC and add it to the end + uint8_t crc = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); + EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = crc; // 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); @@ -433,112 +505,137 @@ void _ems_sendTelegram() { // send the telegram to the UART Tx emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); - EMS_Sys_Status.emsTxPkgs++; + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_WAIT; +} - // 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)) { - // create a new Telegram copying from the last write - _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.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 - EMS_TxQueue.unshift(new_EMS_TxTelegram); // add back to queue making it next in line +/** + * Takes the last write command and turns into a validate request + * placing it on the queue + */ +void _createValidate() { + if (EMS_TxQueue.isEmpty()) { + return; } - EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; + // release the Tx lock + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; + + // get the first in the queue, which is at the head + _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); + + // safety check: only do a validate after a write and when we have a type to validate + if ((EMS_TxTelegram.action != EMS_TX_TELEGRAM_WRITE) || (EMS_TxTelegram.type_validate == EMS_ID_NONE)) { + EMS_TxQueue.shift(); // remove from queue + return; + } + + // create a new Telegram copying from the last write + _EMS_TxTelegram new_EMS_TxTelegram; + new_EMS_TxTelegram.action = EMS_TX_TELEGRAM_VALIDATE; + + // copy old Write record + 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.comparisonValue = EMS_TxTelegram.comparisonValue; + new_EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.comparisonPostRead; + new_EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.comparisonOffset; + + // this is what is different + 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) + + // remove old telegram from queue and add this new read one + EMS_TxQueue.shift(); // remove from queue + EMS_TxQueue.unshift(new_EMS_TxTelegram); // add back to queue making it first to be picked up next (FIFO) } /** - * the main logic that parses the telegram message + * the main logic that parses the telegram message, triggered by an interrupt in emsuart.cpp * length is only data bytes, excluding the BRK * Read commands are asynchronous as they're handled by the interrupt - * When we receive a Poll Request we need to send any Tx packages quickly + * When we receive a Poll Request we need to send any Tx packages quickly within a 200ms window */ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // check if we just received a single byte - // it could well be a Poll request from the boiler which has an ID 0x8B (0x0B | 0x80 to set 8th bit) - // or either a return code like 0x01 or 0x04 from the last Write command issued + // it could well be a Poll request from the boiler to us which will have a value of 0x8B (0x0B | 0x80) + // or either a return code like 0x01 or 0x04 from the last Write command if (length == 1) { uint8_t value = telegram[0]; // 1st byte of data package // 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; + EMS_Sys_Status.emsPollTimestamp = millis(); // store when we received a last poll + EMS_Sys_Status.emsTxCapable = true; - // do we have something to send thats waiting in the Tx queue? if so send it - if (!EMS_TxQueue.isEmpty()) { - _ems_sendTelegram(); // perform the read/write command + // do we have something to send thats waiting in the Tx queue? if so send it if the Queue is not in a wait state + if ((!EMS_TxQueue.isEmpty()) && (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE)) { + _ems_sendTelegram(); // perform the read/write command immediately } else { // nothing to send so just send a poll acknowledgement back if (EMS_Sys_Status.emsPollEnabled) { emsaurt_tx_poll(); } } - } 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 - 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"); - } - EMS_TxQueue.shift(); // write failed so remove from queue. pretty sloppy. + } else if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_WAIT) { + // this may be a single byte 01 (success) or 04 (error) from a recent write command? + if (value == EMS_TX_SUCCESS) { + EMS_Sys_Status.emsTxPkgs++; + // got a success 01. Send a validate to check the value of the last write + emsaurt_tx_poll(); // send a poll to free the EMS bus + _createValidate(); // create a validate Tx request (if needed) + } else if (value == EMS_TX_ERROR) { + // last write failed (04), delete it from queue and dont bother to retry + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + myDebug("** Write command failed from host"); } emsaurt_tx_poll(); // send a poll to free the EMS bus + _removeTxQueue(); // remove from queue } } - return; // all done here, quit. if we haven't processes anything its a poll but not for us + + return; // all done here } // ignore anything that doesn't resemble a proper telegram package // minimal is 5 bytes, excluding CRC at the end - /* - if (length < EMS_MIN_TELEGRAM_LENGTH - 1) { - _debugPrintTelegram("Noisy data:", telegram, length, COLOR_RED); + if (length <= 4) { + //_debugPrintTelegram("Noisy data:", telegram, length, COLOR_RED); return; } - */ - // Assume at this point we have something that vaguely resembles a telegram - // see if we got a telegram as [src] [dest] [type] [offset] [data] [crc] - // so is at least 6 bytes long and the CRC checks out (which is last byte) + // Assume at this point we have something that vaguely resembles a telegram in the format [src] [dest] [type] [offset] [data] [crc] + // validate the CRC, if its bad ignore it uint8_t crc = _crcCalculator(telegram, 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]); - } - myDebug("\n"); + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + _debugPrintTelegram("Corrupt telegram:", telegram, length, COLOR_RED); } - - // 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 + // but still continue to process it + 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 + } + myDebug(raw); + } + + // here we know its a valid incoming telegram of at least 6 bytes + // we use this to see if we always have a connection to the boiler, in case of drop outs + EMS_Sys_Status.emsRxTimestamp = millis(); // timestamp of last read + EMS_Sys_Status.emsBusConnected = true; + + // now lets process it and see what to do next + _processType(telegram, length); } /** @@ -547,220 +644,289 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { */ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { // header - uint8_t src = telegram[0] & 0x7F; - uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes - uint8_t type = telegram[2]; - uint8_t * data = telegram + 4; // data block starts at position 5 + uint8_t src = telegram[0] & 0x7F; + uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes + uint8_t type = telegram[2]; + uint8_t offset = telegram[3]; + uint8_t * data = telegram + 4; // data block starts at position 5 // print detailed telegram data if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { - 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); + // always print + _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 + break; + } + i++; + } + + // if it's a common type (across ems devices) or something specifically for us process it. + // dest will be EMS_ID_NONE and offset 0x00 for a broadcast message + if (typeFound && (commonType || forUs)) { + if ((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); + } + // call callback function to process it + // as we only handle complete telegrams (not partial) check that the offset is 0 + if (offset == EMS_ID_NONE) { + (void)EMS_Types[i].processType_cb(type, data, length - 5); } - i++; } } } +/** + * Remove current Tx telegram from queue and release lock on Tx + */ +void _removeTxQueue() { + if (!EMS_TxQueue.isEmpty()) { + EMS_TxQueue.shift(); // remove item from top of the queue + } + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; +} /** - * deciphers the telegram packet + * deciphers the telegram packet, which has already been checked for valid CRC and has a complete header (min of 5 bytes) * length is only data bytes, excluding the BRK * We only remove from the Tx queue if the read or write was successful */ void _processType(uint8_t * telegram, uint8_t length) { // header - uint8_t src = telegram[0] & 0x7F; // removing 8th bit as we deal with both reads and writes - uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes - uint8_t type = telegram[2]; - uint8_t * data = telegram + 4; // data block starts at position 5 + uint8_t src = telegram[0] & 0x7F; // removing 8th bit as we deal with both reads and writes here - // if its an echo of ourselves from the master, ignore + // if its an echo of ourselves from the master UBA, ignore if (src == EMS_ID_ME) { - // _debugPrintTelegram("Telegram echo:", telegram, length, COLOR_BLUE); + //_debugPrintTelegram("Telegram echo:", telegram, length, COLOR_BLUE); 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 - // 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())) { - _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); // get current Tx package we last sent + // if its a broadcast and we didn't just send anything, process it and exit + if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE) { + _ems_processTelegram(telegram, length); + return; + } - // do the types match? If so we were expecting this response back to us - if (EMS_TxTelegram.type == type) { - emsaurt_tx_poll(); // send Acknowledgement back to free the EMS bus + // release the lock on the TxQueue + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; - // if last action was a read, where just happy that we 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 - if (EMS_TxTelegram.forceRefresh) { - ems_setEmsRefreshed(true); // set the MQTT refresh flag to force sending to MQTT + // at this point we can assume Txstatus is EMS_TX_STATUS_WAIT so we just sent a read/write/validate + // for READ, WRITE or VALIDATE the dest (telegram[1]) is always us, so check for this + // and if not we probably didn't get any response so remove the last Tx from the queue and process the telegram anyway + if ((telegram[1] & 0x7F) != EMS_ID_ME) { + _removeTxQueue(); + _ems_processTelegram(telegram, length); + return; + } + + // first double check we actually have something in the queue + if (EMS_TxQueue.isEmpty()) { + _ems_processTelegram(telegram, length); + return; + } + + // get the Tx telegram we just sent + _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); + + // check action + // if READ, match the current inbound telegram to what we sent + // if WRITE, should not happen + // if VALIDATE, check the contents + if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) { + uint8_t type = telegram[2]; + if ((src == EMS_TxTelegram.dest) && (type == EMS_TxTelegram.type)) { + // all checks out, read was successful, remove tx from queue and continue to process telegram + _removeTxQueue(); + EMS_Sys_Status.emsRxPgks++; // increment counter + // myDebug("** Read from 0x%02X ok", type); + ems_setEmsRefreshed(EMS_TxTelegram.forceRefresh); // does mqtt need refreshing? + } else { + // read not OK, we didn't get back a telegram we expected + // leave on queue and try again, but continue to process what we received as it may be important + EMS_Sys_Status.txRetryCount++; + // if retried too many times, give up and remove it + if (EMS_Sys_Status.txRetryCount >= TX_WRITE_TIMEOUT_COUNT) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Read failed. Giving up, removing from queue"); } - EMS_TxQueue.shift(); // remove read from queue - } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { - // this read was for a validate. Do a compare on the 1 byte result from the last write - uint8_t dataReceived = data[0]; // only a single byte is returned after a read - if (EMS_TxTelegram.comparisonValue == dataReceived) { - // 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); - } - ems_doReadCommand(EMS_TxTelegram.comparisonPostRead, - EMS_TxTelegram.dest, - true); // get values and force a refresh to MQTT - } 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. ", - EMS_TxTelegram.comparisonValue, - dataReceived); - } - if (emsRxRetryCount++ >= RX_READ_TIMEOUT_COUNT) { - // give up - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Giving up!\n"); - } - 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); - } - 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 - } + _removeTxQueue(); + } else { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("...Retrying read. Attempt %d/%d...", EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); } } } - // telegram was for us, but seems we didn't ask for it - } else { - // we didn't request it - _ems_processTelegram(telegram, length); // and process and print it + _ems_processTelegram(telegram, length); // process it always } + + if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { + // should not get here, since this is handled earlier receiving a 01 or 04 + myDebug("** Error ! Write - should not be here"); + } + + if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) { + // this is a read telegram which we use to validate the last write + uint8_t * data = telegram + 4; // data block starts at position 5 + uint8_t dataReceived = data[0]; // only a single byte is returned after a read + if (EMS_TxTelegram.comparisonValue == dataReceived) { + // validate was successful, the write changed the value + _removeTxQueue(); // now we can remove the Tx validate command the queue + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Write to 0x%02X was successful", EMS_TxTelegram.dest); + } + // follow up with the post read command + ems_doReadCommand(EMS_TxTelegram.comparisonPostRead, EMS_TxTelegram.dest, true); + } 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", + EMS_TxTelegram.comparisonValue, + dataReceived); + } + if (++EMS_Sys_Status.txRetryCount > TX_WRITE_TIMEOUT_COUNT) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Write failed. Giving up, removing from queue"); + } + _removeTxQueue(); + } else { + // retry, turn the validate back into a write and try again + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("...Retrying write. Attempt %d/%d...", EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT); + } + 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 + } + } + } + + emsaurt_tx_poll(); // send Acknowledgement back to free the EMS bus since we have the telegram } + /** * Check if hot tap water or heating is active - * using a quick hack: - * heating is on if Selected Flow Temp >= 70 (in my case) - * tap water is on if Selected Flow Temp = 0 and Selected Burner Power >= 115 + * using a quick hack for checking the heating. Selected Flow Temp >= 70 */ -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)); +void _checkActive() { + // hot tap water, using flow to check insread of the burner power + EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); // 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)); } /** * UBAParameterWW - type 0x33 - warm water parameters * received only after requested (not broadcasted) */ -void _process_UBAParameterWW(uint8_t * data, uint8_t length) { +void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.wWActivated = (data[1] == 0xFF); // 0xFF means on EMS_Boiler.wWSelTemp = data[2]; EMS_Boiler.wWCircPump = (data[6] == 0xFF); // 0xFF means on EMS_Boiler.wWDesiredTemp = data[8]; + EMS_Boiler.wWComfort = (data[EMS_OFFSET_UBAParameterWW_wwComfort] == 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 type, 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 +} + +/* + * UBAParametersMessage - type 0x16 + */ +void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Boiler.heating_temp = data[1]; + EMS_Boiler.pump_mod_max = data[9]; + EMS_Boiler.pump_mod_min = data[10]; } /** * UBAMonitorWWMessage - type 0x34 - warm water monitor. 19 bytes long * received every 10 seconds */ -void _process_UBAMonitorWWMessage(uint8_t * data, uint8_t length) { +void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.wWCurTmp = _toFloat(1, data); EMS_Boiler.wWStarts = _toLong(13, data); EMS_Boiler.wWWorkM = _toLong(10, data); EMS_Boiler.wWOneTime = bitRead(data[5], 1); + EMS_Boiler.wWCurFlow = data[9]; } /** * UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long) * received every 10 seconds */ -void _process_UBAMonitorFast(uint8_t * data, uint8_t length) { +void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.selFlowTemp = data[0]; EMS_Boiler.curFlowTemp = _toFloat(1, data); EMS_Boiler.retTemp = _toFloat(13, data); @@ -778,6 +944,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.serviceCodeChar[0] = char(data[18]); // ascii character 1 + EMS_Boiler.serviceCodeChar[1] = char(data[19]); // ascii character 2 + if (data[17] == 0xFF) { // missing value for system pressure EMS_Boiler.sysPress = 0; } else { @@ -785,14 +955,14 @@ void _process_UBAMonitorFast(uint8_t * data, uint8_t length) { } // at this point do a quick check to see if the hot water or heating is active - (void)_checkActive(); + _checkActive(); } /** * UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long) * received every 60 seconds */ -void _process_UBAMonitorSlow(uint8_t * data, uint8_t length) { +void _process_UBAMonitorSlow(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.extTemp = _toFloat(0, data); // 0x8000 if not available EMS_Boiler.boilTemp = _toFloat(2, data); // 0x8000 if not available EMS_Boiler.pumpMod = data[9]; @@ -801,106 +971,295 @@ void _process_UBAMonitorSlow(uint8_t * data, uint8_t length) { EMS_Boiler.heatWorkMin = _toLong(19, data); } + /** - * RC20StatusMessage - type 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long + * type 0xB1 - data from the RC10 thermostat (0x17) * For reading the temp values only * received every 60 seconds */ -void _process_RC20StatusMessage(uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[1]) / (float)2; - EMS_Thermostat.curr_roomTemp = _toFloat(2, data); +void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_setpoint]) / (float)2; + EMS_Thermostat.curr_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_curr]) / (float)10; - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /** - * RC30StatusMessage - type 0x41 - data from the RC30 thermostat (0x10) - 14 bytes long + * type 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long * For reading the temp values only * received every 60 seconds */ -void _process_RC30StatusMessage(uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[1]) / (float)2; - EMS_Thermostat.curr_roomTemp = _toFloat(2, data); +void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC20StatusMessage_setpoint]) / (float)2; + EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC20StatusMessage_curr, data); - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /** - * EasyStatusMessage - type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long + * type 0x41 - data from the RC30 thermostat (0x10) - 14 bytes long + * For reading the temp values only + * received every 60 seconds + */ +void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC30StatusMessage_setpoint]) / (float)2; + EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC30StatusMessage_curr, data); + + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT +} + +/** + * type 0x3E - data from the RC35 thermostat (0x10) - 16 bytes + * For reading the temp values only + * received every 60 seconds + */ +void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC35StatusMessage_setpoint]) / (float)2; + + // check if temp sensor is unavailable + if ((data[0] == 0x7D) && (data[1] = 0x00)) { + EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET; + } else { + EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC35StatusMessage_curr, data); + } + EMS_Thermostat.day_mode = bitRead(data[EMS_OFFSET_RC35Get_mode_day], 1); //get day mode flag + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT +} + +/** + * type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long * The Easy has a digital precision of its floats to 2 decimal places, so values is divided by 100 */ -void _process_EasyStatusMessage(uint8_t * data, uint8_t length) { - EMS_Thermostat.curr_roomTemp = ((float)(((data[8] << 8) + data[9]))) / 100; - EMS_Thermostat.setpoint_roomTemp = ((float)(((data[10] << 8) + data[11]))) / 100; +void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.curr_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_curr] << 8) + data[9]))) / 100; + EMS_Thermostat.setpoint_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_setpoint] << 8) + data[11]))) / 100; - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /** - * RC20Temperature - type 0xA8 - for reading the mode from the RC20 thermostat (0x17) + * type 0xB0 - for reading the mode from the RC10 thermostat (0x17) * received only after requested */ -void _process_RC20Set(uint8_t * data, uint8_t length) { +void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length) { + // mode not implemented yet +} + +/** + * type 0xA8 - for reading the mode from the RC20 thermostat (0x17) + * received only after requested + */ +void _process_RC20Set(uint8_t type, uint8_t * data, uint8_t length) { EMS_Thermostat.mode = data[EMS_OFFSET_RC20Set_mode]; } /** - * RC30Temperature - type 0xA7 - for reading the mode from the RC30 thermostat (0x10) + * type 0xA7 - for reading the mode from the RC30 thermostat (0x10) * received only after requested */ -void _process_RC30Set(uint8_t * data, uint8_t length) { +void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length) { EMS_Thermostat.mode = data[EMS_OFFSET_RC30Set_mode]; } /** - * RCOutdoorTempMessage - type 0xA3 - for external temp settings from the the RC* thermostats + * type 0x3D - for reading the mode from the RC35 thermostat (0x10) + * Working Mode Heating Circuit 1 (HC1) + * received only after requested */ -void _process_RCOutdoorTempMessage(uint8_t * data, uint8_t length) { +void _process_RC35Set(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.mode = data[EMS_OFFSET_RC35Set_mode]; +} + +/** + * type 0xA3 - for external temp settings from the the RC* thermostats + */ +void _process_RCOutdoorTempMessage(uint8_t type, uint8_t * data, uint8_t length) { // add support here if you're reading external sensors } /** - * 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 + * type 0x02 - get the firmware version and type of an EMS device + * look up known devices via the product id and setup if not already set */ -void _process_Version(uint8_t * data, uint8_t length) { +void _process_Version(uint8_t type, 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; + } + + bool do_save = false; + uint8_t product_id = data[0]; + char version[10] = {0}; + snprintf(version, sizeof(version), "%02d.%02d", data[1], data[2]); + + // see if its a known boiler + int i = 0; + bool typeFound = false; + while (i < _Boiler_Types_max) { + if (Boiler_Types[i].product_id == product_id) { + typeFound = true; // we have a matching product id. i is the index. + break; } + i++; + } + + if (typeFound) { + // its a boiler + myDebug("Boiler type device found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + Boiler_Types[i].model_string, + Boiler_Types[i].type_id, + product_id, + version); + + // if its a boiler set it + // it will take the first one found in the list + if ((EMS_Boiler.type_id == EMS_ID_NONE) || (EMS_Boiler.type_id == Boiler_Types[i].type_id)) { + myDebug("* Setting Boiler type to Model %s, TypeID 0x%02X, Product ID %d, Version %s", + Boiler_Types[i].model_string, + Boiler_Types[i].type_id, + product_id, + version); + + EMS_Boiler.type_id = Boiler_Types[i].type_id; + EMS_Boiler.product_id = Boiler_Types[i].product_id; + strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); + + do_save = true; + + ems_getBoilerValues(); // get Boiler values that we would usually have to wait for + } + return; + } + + // its not a boiler, maybe its a known thermostat? + i = 0; + while (i < _Thermostat_Types_max) { + if (Thermostat_Types[i].product_id == product_id) { + typeFound = true; // we have a matching product id. i is the index. + break; + } + i++; + } + + if (typeFound) { + // its a known thermostat + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Thermostat found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + Thermostat_Types[i].model_string, + Thermostat_Types[i].type_id, + product_id, + version); + } + + // if we don't have a thermostat set, use this one + if ((EMS_Thermostat.type_id == EMS_ID_NONE) || (EMS_Thermostat.model_id == EMS_MODEL_NONE) + || (EMS_Thermostat.type_id == Thermostat_Types[i].type_id)) { + myDebug("* Setting Thermostat type to Model %s, TypeID 0x%02X, Product ID %d, Version %s", + Thermostat_Types[i].model_string, + Thermostat_Types[i].type_id, + product_id, + version); + + EMS_Thermostat.model_id = Thermostat_Types[i].model_id; + EMS_Thermostat.type_id = Thermostat_Types[i].type_id; + EMS_Thermostat.read_supported = Thermostat_Types[i].read_supported; + EMS_Thermostat.write_supported = Thermostat_Types[i].write_supported; + EMS_Thermostat.product_id = product_id; + strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); + + do_save = true; + + // get Thermostat values (if supported) + ems_getThermostatValues(); + } + } else { + myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", type, product_id, version); + } + + // if the boiler or thermostat values have changed, save them to SPIFFS + if (do_save) { + myESP.fs_saveConfig(); } } -/** - * UBASetPoint 0x1A, for RC20 +/* + * Figure out the boiler and thermostat types */ -void _process_SetPoints(uint8_t * data, uint8_t length) { - if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { +void ems_discoverModels() { + // boiler + ems_doReadCommand(EMS_TYPE_Version, EMS_Boiler.type_id); // get version details of boiler + + // thermostat + // if it hasn't been set, auto discover it + if (EMS_Thermostat.type_id == EMS_ID_NONE) { + ems_scanDevices(); // auto-discover it + } else { + // set the model as hardcoded (see my_devices.h) and fetch the version and product id + ems_doReadCommand(EMS_TYPE_Version, EMS_Thermostat.type_id); + } +} + +/* + * Given a thermostat model ID go and fetch its characteristics + */ +void _ems_setThermostatModel(uint8_t thermostat_modelid) { + bool found = false; + uint8_t i = 0; + const _Thermostat_Type * thermostat_type; + while (i < _Thermostat_Types_max) { + thermostat_type = &Thermostat_Types[i]; + if (thermostat_type->model_id == thermostat_modelid) { + found = true; // we have a matching product id + break; + } + i++; + } + + if (!found) { + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Unknown thermostat model specified. Trying a scan..."); + } + ems_scanDevices(); // auto-discover it + return; + } + + // set the thermostat + if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { + myDebug("Setting Thermostat. Model %s with TypeID 0x%02X, Product ID %d", + thermostat_type->model_string, + thermostat_type->type_id, + thermostat_type->product_id); + } + + // set its capabilities + EMS_Thermostat.model_id = thermostat_type->model_id; + EMS_Thermostat.type_id = thermostat_type->type_id; + EMS_Thermostat.read_supported = thermostat_type->read_supported; + EMS_Thermostat.write_supported = thermostat_type->write_supported; +} + +/** + * UBASetPoint 0x1A + */ +void _process_SetPoints(uint8_t type, uint8_t * data, uint8_t length) { + /* + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { if (length != 0) { uint8_t setpoint = data[0]; uint8_t hk_power = data[1]; uint8_t ww_power = data[2]; - myDebug(" SetPoint=%d, hk_power=%d ww_power=%d\n", setpoint, hk_power, ww_power); + myDebug(" SetPoint=%d, hk_power=%d, ww_power=%d", setpoint, hk_power, ww_power); } - myDebug("\n"); } + */ } /** * process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long * common for all thermostats */ -void _process_RCTime(uint8_t * data, uint8_t length) { - if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_EASY) { +void _process_RCTime(uint8_t type, uint8_t * data, uint8_t length) { + if ((EMS_Thermostat.model_id == EMS_MODEL_EASY) || (EMS_Thermostat.model_id == EMS_MODEL_BOSCHEASY)) { return; // not supported } @@ -910,16 +1269,6 @@ void _process_RCTime(uint8_t * data, uint8_t length) { EMS_Thermostat.day = data[3]; EMS_Thermostat.month = data[1]; EMS_Thermostat.year = data[0]; - - // we can optional set the time based on the thermostat's time if we want. - /* - setTime(EMS_Thermostat.hour, - EMS_Thermostat.minute, - EMS_Thermostat.second, - EMS_Thermostat.day, - EMS_Thermostat.month, - EMS_Thermostat.year + 2000); - */ } /** @@ -927,27 +1276,41 @@ 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); + if (EMS_TxQueue.size() == 0) { + myDebug("Tx queue is empty."); + return; + } + + 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", - i, + "comparisonValue=%d type_validate=0x%02x comparisonPostRead=0x%02x @ %s", + i + 1, sType, EMS_TxTelegram.dest & 0x7F, EMS_TxTelegram.type, @@ -955,45 +1318,156 @@ 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_getThermostatEnabled()) { + return; } + + if (!EMS_Thermostat.read_supported) { + myDebug("Read operations not yet supported for this model thermostat"); + return; + } + + uint8_t 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_RC35) || (model_id == EMS_MODEL_ES73)) { + ems_doReadCommand(EMS_TYPE_RC35StatusMessage, type); // to get the setpoint temp + ems_doReadCommand(EMS_TYPE_RC35Set, type); // to get the mode + } else if ((model_id == EMS_MODEL_EASY) || (model_id == EMS_MODEL_BOSCHEASY)) { + 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 - break; - } - i++; - } - if (typeFound) { - myDebug("%s [ID 0x%02X]", Thermostat_Types[i].typeString, Thermostat_Types[i].id); +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_UBAParametersMessage, EMS_Boiler.type_id); // get MC10 boiler values + ems_doReadCommand(EMS_TYPE_UBATotalUptimeMessage, EMS_Boiler.type_id); // get uptime from boiler +} + +/** + * returns current thermostat type as a string + */ +char * ems_getThermostatDescription(char * buffer) { + uint8_t size = 128; + if (!ems_getThermostatEnabled()) { + strlcpy(buffer, "", size); } else { - myDebug("Unknown? [ID 0x%02X]", Thermostat_Types[i].id); + // find the boiler details + int i = 0; + bool found = false; + + // scan through known ID types + while (i < _Thermostat_Types_max) { + if (Thermostat_Types[i].product_id == EMS_Thermostat.product_id) { + found = true; // we have a match + break; + } + i++; + } + if (found) { + strlcpy(buffer, Thermostat_Types[i].model_string, size); + } else { + strlcpy(buffer, "Generic Type", size); + } + + char tmp[6] = {0}; + strlcat(buffer, " [Type ID: 0x", size); + strlcat(buffer, _hextoa(EMS_Thermostat.type_id, tmp), size); + strlcat(buffer, "] Product ID:", size); + strlcat(buffer, itoa(EMS_Thermostat.product_id, tmp, 10), size); + strlcat(buffer, " Version:", size); + strlcat(buffer, EMS_Thermostat.version, size); + } + + return buffer; +} + +/** + * returns current boiler type as a string + */ +char * ems_getBoilerDescription(char * buffer) { + uint8_t size = 128; + if (!ems_getBoilerEnabled()) { + strlcpy(buffer, "", size); + } else { + // find the boiler details + int i = 0; + bool found = false; + + // scan through known ID types + while (i < _Boiler_Types_max) { + if (Boiler_Types[i].product_id == EMS_Boiler.product_id) { + found = true; // we have a match + break; + } + i++; + } + if (found) { + strlcpy(buffer, Boiler_Types[i].model_string, size); + } else { + strlcpy(buffer, "Generic Type", size); + } + + char tmp[6] = {0}; + strlcat(buffer, " [Type ID: 0x", size); + strlcat(buffer, _hextoa(EMS_Boiler.type_id, tmp), size); + strlcat(buffer, "] Product ID:", size); + strlcat(buffer, itoa(EMS_Boiler.product_id, tmp, 10), size); + strlcat(buffer, " Version:", size); + strlcat(buffer, EMS_Boiler.version, size); + } + + return buffer; +} + +/* + * Find the versions of our connected devices + */ +void ems_scanDevices() { + myDebug("Started scan of EMS bus for known devices"); + + std::list Device_Ids; // new list + + // copy over boilers + for (_Boiler_Type bt : Boiler_Types) { + Device_Ids.push_back(bt.type_id); + } + + // copy over thermostats + for (_Thermostat_Type tt : Thermostat_Types) { + Device_Ids.push_back(tt.type_id); + } + // remove duplicates and reserved IDs (like our own device) + Device_Ids.sort(); + Device_Ids.unique(); + Device_Ids.remove(EMS_MODEL_NONE); + + // send the read command with Version command + for (uint8_t type_id : Device_Ids) { + ems_doReadCommand(EMS_TYPE_Version, type_id); } } @@ -1001,25 +1475,30 @@ void ems_printThermostatType() { * Print out all handled types */ void ems_printAllTypes() { - myDebug("These %d telegram type IDs are recognized:\n", _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"); - } else { - strcpy(s, "Common"); - } - myDebug(" %s:\ttype ID %02X (%s)\n", s, EMS_Types[i].type, EMS_Types[i].typeString); + myDebug("\nThese %d boiler type devices are in the library:", _Boiler_Types_max); + + for (i = 0; i < _Boiler_Types_max; i++) { + myDebug(" %s, type ID:0x%02X Product ID:%d", Boiler_Types[i].model_string, Boiler_Types[i].type_id, Boiler_Types[i].product_id); } - myDebug("\nThese %d telegram Thermostats are natively supported:\n", _Thermostat_Types_max); + myDebug("\nThese telegram type IDs are recognized for the selected boiler:"); + for (i = 0; i < _EMS_Types_max; i++) { + if ((EMS_Types[i].model_id == EMS_MODEL_ALL) || (EMS_Types[i].model_id == EMS_MODEL_UBA)) { + myDebug(" type %02X (%s)", EMS_Types[i].type, EMS_Types[i].typeString); + } + } + + myDebug("\nThese %d thermostats models are 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); + myDebug(" %s, type ID:0x%02X Product ID:%d Read/Write support:%c%c", + Thermostat_Types[i].model_string, + Thermostat_Types[i].type_id, + Thermostat_Types[i].product_id, + (Thermostat_Types[i].read_supported) ? 'r' : ' ', + (Thermostat_Types[i].write_supported) ? 'w' : ' '); } } @@ -1028,21 +1507,23 @@ 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; } _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter // 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 @@ -1062,22 +1543,26 @@ void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh) { /** * Send a raw telegram to the bus + * telegram is a string of hex values */ 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 + EMS_Sys_Status.txRetryCount = 0; // reset retry counter // get first value, which should be the src - if (p = strtok(telegram, " ,")) { // delimiter - strcpy(value, p); + if ( (p = strtok(telegram, " ,")) ) { // delimiter + strlcpy(value, p, sizeof(value)); EMS_TxTelegram.data[0] = (uint8_t)strtol(value, 0, 16); } // and interate until end while (p != 0) { - if (p = strtok(NULL, " ,")) { - strcpy(value, p); + if ( (p = strtok(NULL, " ,")) ) { + strlcpy(value, p, sizeof(value)); uint8_t val = (uint8_t)strtol(value, 0, 16); EMS_TxTelegram.data[++count] = val; if (count == 1) { @@ -1103,74 +1588,100 @@ 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_getThermostatEnabled()) { 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_Sys_Status.txRetryCount = 0; // reset retry counter + + uint8_t 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"); + + // when doing a comparison to validate the new temperature we call a different type + + 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; + } else if (model_id == EMS_MODEL_RC10) { + EMS_TxTelegram.type = EMS_TYPE_RC10Set; + EMS_TxTelegram.offset = EMS_OFFSET_RC10Set_temp; + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC10StatusMessage; + } 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; + } else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) { + EMS_TxTelegram.type = EMS_TYPE_RC35Set; + if (EMS_Thermostat.day_mode == 0) { + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_night; + } else if (EMS_Thermostat.day_mode == 1) { + EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_day; + } + + EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage; + } + + 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_getThermostatEnabled()) { return; } - myDebug("Setting thermostat mode to %d\n", mode); + if (!EMS_Thermostat.write_supported) { + myDebug("Write not supported for this model Thermostat"); + return; + } + + uint8_t 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_Sys_Status.txRetryCount = 0; // reset retry counter 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) || (model_id == EMS_MODEL_ES73)) { + 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 +1698,18 @@ 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_Sys_Status.txRetryCount = 0; // reset retry counter 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; @@ -1211,22 +1724,47 @@ void ems_setWarmWaterTemp(uint8_t temperature) { EMS_TxQueue.push(EMS_TxTelegram); } +/** + * Set the warm water mode to comfort to Eco/Comfort + */ +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.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter + + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; + EMS_TxTelegram.dest = EMS_Boiler.type_id; + EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; + EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwComfort; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate + EMS_TxTelegram.dataValue = + (comfort ? EMS_VALUE_UBAParameterWW_wwComfort_Comfort : EMS_VALUE_UBAParameterWW_wwComfort_Eco); // 0x00 is on, 0xD8 is off + + EMS_TxQueue.push(EMS_TxTelegram); +} + /** * Activate / De-activate the Warm Water 0x33 * true = on, false = off */ void ems_setWarmWaterActivated(bool activated) { - myDebug("Setting boiler warm water %s\n", activated ? "on" : "off"); + myDebug("Setting boiler warm water %s", activated ? "on" : "off"); _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx + EMS_TxTelegram.timestamp = millis(); // set timestamp + EMS_Sys_Status.txRetryCount = 0; // reset retry counter EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; - EMS_TxTelegram.dest = EMS_ID_BOILER; + 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; EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate EMS_TxTelegram.dataValue = (activated ? 0xFF : 0x00); // 0xFF is on, 0x00 is off + EMS_TxQueue.push(EMS_TxTelegram); } @@ -1236,17 +1774,19 @@ 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 + EMS_Sys_Status.txRetryCount = 0; // reset retry counter // 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 @@ -1257,7 +1797,6 @@ void ems_setWarmTapWaterActivated(bool activated) { EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.type; EMS_TxTelegram.forceRefresh = true; // send new value to MQTT after successful write - // create header EMS_TxTelegram.data[0] = EMS_ID_ME; // src EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; // dest @@ -1277,32 +1816,3 @@ void ems_setWarmTapWaterActivated(bool activated) { EMS_TxQueue.push(EMS_TxTelegram); // add to queue } - -/** - * experimental code for debugging - not in production - */ -void ems_setExperimental(uint8_t value) { - /* - _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx - - EMS_TxTelegram.action = EMS_TX_TELEGRAM_READ; // read command - EMS_TxTelegram.dest = EMS_ID_THERMOSTAT; // set 8th bit to indicate a read - EMS_TxTelegram.offset = 0; // 0 for all data - EMS_TxTelegram.length = 8; - EMS_TxTelegram.type = 0xF0; - EMS_TxTelegram.type_validate = EMS_ID_NONE; - - // EMS Plus test - // Sending read to 0x18: telegram: 0B 98 F0 00 01 B9 63 DB (len 8) - EMS_TxTelegram.data[0] = EMS_ID_ME; // src - EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; // dest - EMS_TxTelegram.data[2] = 0xF0; // marker - EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; // offset - EMS_TxTelegram.data[4] = 0x01; // hi byte - EMS_TxTelegram.data[5] = 0xB9; // low byte - - EMS_TxTelegram.data[6] = 99; // max length - - EMS_TxQueue.push(EMS_TxTelegram); - */ -} diff --git a/src/ems.h b/src/ems.h index bce7ae158..5147223b0 100644 --- a/src/ems.h +++ b/src/ems.h @@ -1,7 +1,11 @@ /* - * Header file for EMS.cpp + * Header file for ems.cpp + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP + * + * See ChangeLog.md for history + * See README.md for Acknowledgments * - * You shouldn't need to change much in this file */ #pragma once @@ -9,83 +13,53 @@ #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 the "Service Key" +#define EMS_ID_DEFAULT_BOILER 0x08 -#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_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC -#define EMS_TX_MAXBUFFERSIZE 128 // max size of the buffer. packets are 32 bits - -#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) - -// define here the EMS telegram types you need - -// Common for all EMS devices -#define EMS_TYPE_Version 0x02 // version of the UBA controller (boiler) - -/* - * Boiler... - */ -#define EMS_TYPE_UBAMonitorFast 0x18 // is an automatic monitor broadcast -#define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast -#define EMS_TYPE_UBAMonitorWWMessage 0x34 // is an automatic monitor broadcast -#define EMS_TYPE_UBAMaintenanceStatusMessage 0x1C // is an automatic monitor broadcast -#define EMS_TYPE_UBAParameterWW 0x33 -#define EMS_TYPE_UBATotalUptimeMessage 0x14 -#define EMS_TYPE_UBAMaintenanceSettingsMessage 0x15 -#define EMS_TYPE_UBAParametersMessage 0x16 -#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 - -/* - * Thermostat... - */ - -// Common for all thermostats -#define EMS_TYPE_RCTime 0x06 // is an automatic thermostat broadcast -#define EMS_TYPE_RCOutdoorTempMessage 0xA3 // is an automatic thermostat broadcast, outdoor external temp - -// RC20 specific -#define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps -#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode -#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode -#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature - -// RC30 specific -#define EMS_TYPE_RC30StatusMessage 0x41 // is an automatic thermostat broadcast giving us temps -#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode -#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode -#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature - -// Easy specific -#define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat +// max length of a telegram, including CRC, for Rx and Tx. +#define EMS_MAX_TELEGRAM_LENGTH 99 // 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 + +#define EMS_THERMOSTAT_READ_YES true +#define EMS_THERMOSTAT_READ_NO false +#define EMS_THERMOSTAT_WRITE_YES true +#define EMS_THERMOSTAT_WRITE_NO false + +// 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 + +#define EMS_TX_TELEGRAM_QUEUE_MAX 50 // max size of Tx FIFO queue + +//#define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_VERBOSE +#define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_NONE /* EMS UART transfer status */ typedef enum { - EMS_RX_IDLE, - EMS_RX_ACTIVE // Rx package is being sent + EMS_RX_STATUS_IDLE, + EMS_RX_STATUS_BUSY // Rx package is being received } _EMS_RX_STATUS; typedef enum { - EMS_TX_IDLE, - EMS_TX_ACTIVE, // Tx package being sent, no break sent - EMS_TX_SUCCESS, - EMS_TX_ERROR + EMS_TX_STATUS_IDLE, // ready + EMS_TX_STATUS_WAIT // waiting for response from last Tx } _EMS_TX_STATUS; +#define EMS_TX_SUCCESS 0x01 // EMS single byte after a Tx Write indicating a success +#define EMS_TX_ERROR 0x04 // EMS single byte after a Tx Write indicating an error + typedef enum { EMS_TX_TELEGRAM_INIT, // just initialized EMS_TX_TELEGRAM_READ, // doing a read request @@ -107,20 +81,22 @@ typedef enum { typedef struct { _EMS_RX_STATUS emsRxStatus; _EMS_TX_STATUS emsTxStatus; - uint16_t emsRxPgks; // received - uint16_t emsTxPkgs; // sent - uint16_t emxCrcErr; // CRC errors - bool emsPollEnabled; // flag enable the response to poll messages - bool emsTxEnabled; // flag if we're allowing sending of Tx packages - bool emsThermostatEnabled; // if there is a RCxx thermostat active - bool emsBoilerEnabled; // is the boiler online - _EMS_SYS_LOGGING emsLogging; // logging - bool emsRefreshed; // fresh data, needs to be pushed out to MQTT + uint16_t emsRxPgks; // received + uint16_t emsTxPkgs; // sent + uint16_t emxCrcErr; // CRC errors + bool emsPollEnabled; // flag enable the response to poll messages + _EMS_SYS_LOGGING emsLogging; // logging + bool emsRefreshed; // fresh data, needs to be pushed out to MQTT + bool emsBusConnected; // is there an active bus + unsigned long emsRxTimestamp; // timestamp of last EMS message received + unsigned long emsPollTimestamp; // timestamp of last EMS poll sent to us + bool emsTxCapable; // able to send via Tx + uint8_t txRetryCount; // # times the last Tx was re-sent } _EMS_Sys_Status; // The Tx send package typedef struct { - _EMS_TX_TELEGRAM_ACTION action; // read or write + _EMS_TX_TELEGRAM_ACTION action; // read, write, validate, init uint8_t dest; uint8_t type; uint8_t offset; @@ -130,11 +106,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; + + // default empty Tx const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { EMS_TX_TELEGRAM_INIT, // action @@ -147,11 +125,28 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = { 0, // comparisonValue 0, // comparisonOffset EMS_ID_NONE, // comparisonPostRead - false, // hasSent false, // forceRefresh + 0, // timestamp {0x00} // data }; +typedef struct { + uint8_t model_id; + uint8_t product_id; + uint8_t type_id; + char model_string[50]; +} _Boiler_Type; + +// Definition for thermostat type +typedef struct { + uint8_t model_id; + uint8_t product_id; + uint8_t type_id; + char model_string[50]; + bool read_supported; + bool write_supported; +} _Thermostat_Type; + /* * Telegram package defintions */ @@ -160,48 +155,69 @@ 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 + char serviceCodeChar[2]; // 2 character 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 + + // UBAParametersMessage + uint8_t heating_temp; // Heating temperature setting on the boiler + uint8_t pump_mod_max; // Boiler circuit pump modulation max. power + uint8_t pump_mod_min; // Boiler circuit pump modulation min. power // 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; // this is typically always 0x08 + uint8_t product_id; } _EMS_Boiler; // Thermostat data typedef struct { - uint8_t type; // thermostat type (RC30, Easy etc) + uint8_t type_id; // the type ID of the thermostat + uint8_t model_id; // which Thermostat type + uint8_t product_id; + bool read_supported; + bool write_supported; + char version[10]; float setpoint_roomTemp; // current set temp float curr_roomTemp; // current room temp uint8_t mode; // 0=low, 1=manual, 2=auto + bool day_mode; // 0=night, 1=day uint8_t hour; uint8_t minute; uint8_t second; @@ -210,35 +226,16 @@ typedef struct { uint8_t year; } _EMS_Thermostat; -// call back function signature -typedef void (*EMS_processType_cb)(uint8_t * data, uint8_t length); +// call back function signature for processing telegram types +typedef void (*EMS_processType_cb)(uint8_t type, uint8_t * data, uint8_t length); // Definition for each EMS type, including the relative callback function typedef struct { - uint8_t src; + uint8_t 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; - -// 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" -#define COLOR_BOLD_ON "\x1B[1m" -#define COLOR_BOLD_OFF "\x1B[21m" +} _EMS_Type; // function definitions extern void ems_parseTelegram(uint8_t * telegram, uint8_t len); @@ -251,32 +248,42 @@ void ems_setThermostatMode(uint8_t mode); void ems_setWarmWaterTemp(uint8_t temperature); void ems_setWarmWaterActivated(bool activated); void ems_setWarmTapWaterActivated(bool activated); -void ems_setExperimental(uint8_t value); void ems_setPoll(bool b); void ems_setTxEnabled(bool b); -void ems_setThermostatEnabled(bool b); void ems_setLogging(_EMS_SYS_LOGGING loglevel); void ems_setEmsRefreshed(bool b); +void ems_setWarmWaterModeComfort(bool comfort); +bool ems_checkEMSBUSAlive(); +void ems_setModels(); 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(); +uint8_t ems_getThermostatModel(); +void ems_discoverModels(); +bool ems_getTxCapable(); -void ems_printAllTypes(); -void ems_printThermostatType(); -void ems_printTxQueue(); +void ems_scanDevices(); +void ems_printAllTypes(); +char * ems_getThermostatDescription(char * buffer); +void ems_printTxQueue(); +char * ems_getBoilerDescription(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_findBoilerModel(uint8_t model_id); +bool _ems_setModel(uint8_t model_id); +void _ems_setThermostatModel(uint8_t thermostat_modelid); +void _removeTxQueue(); // global so can referenced in other classes extern _EMS_Sys_Status EMS_Sys_Status; diff --git a/src/ems_devices.h b/src/ems_devices.h new file mode 100644 index 000000000..4ac8ab238 --- /dev/null +++ b/src/ems_devices.h @@ -0,0 +1,146 @@ +/* + * General information about known EMS devices + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP + * + * See ChangeLog.md for History + * See README.md for Acknowledgments + * + */ + +#pragma once + +#include "ems.h" + + +/* + * Common + */ +#define EMS_TYPE_Version 0x02 + +/* + * Boiler... + */ +#define EMS_TYPE_UBAMonitorFast 0x18 // is an automatic monitor broadcast +#define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast +#define EMS_TYPE_UBAMonitorWWMessage 0x34 // is an automatic monitor broadcast +#define EMS_TYPE_UBAMaintenanceStatusMessage 0x1C // is an automatic monitor broadcast +#define EMS_TYPE_UBAParameterWW 0x33 +#define EMS_TYPE_UBATotalUptimeMessage 0x14 +#define EMS_TYPE_UBAMaintenanceSettingsMessage 0x15 +#define EMS_TYPE_UBAParametersMessage 0x16 +#define EMS_TYPE_UBASetPoints 0x1A +#define EMS_TYPE_UBAFunctionTest 0x1D + +#define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature +#define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated +#define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode +#define EMS_VALUE_UBAParameterWW_wwComfort_Comfort 0x00 // the value for comfort +#define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco + +/* + * Thermostats... + */ + +// Common for all thermostats +#define EMS_TYPE_RCTime 0x06 // is an automatic thermostat broadcast +#define EMS_TYPE_RCOutdoorTempMessage 0xA3 // is an automatic thermostat broadcast, outdoor external temp + +// RC10 specific +#define EMS_TYPE_RC10StatusMessage 0xB1 // is an automatic thermostat broadcast giving us temps +#define EMS_TYPE_RC10Set 0xB0 // for setting values like temp and mode +#define EMS_OFFSET_RC10Set_temp 4 // position of thermostat setpoint temperature +#define EMS_TYPE_RC10StatusMessage_setpoint 1 // setpoint temp +#define EMS_TYPE_RC10StatusMessage_curr 3 // current temp + +// RC20 specific +#define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps +#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode +#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode +#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature +#define EMS_TYPE_RC20StatusMessage_setpoint 1 // setpoint temp +#define EMS_TYPE_RC20StatusMessage_curr 2 // current temp + +// RC30 specific +#define EMS_TYPE_RC30StatusMessage 0x41 // is an automatic thermostat broadcast giving us temps +#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode +#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode +#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature +#define EMS_TYPE_RC30StatusMessage_setpoint 1 // setpoint temp +#define EMS_TYPE_RC30StatusMessage_curr 2 // current temp + +// RC35 specific +#define EMS_TYPE_RC35StatusMessage 0x3E // is an automatic thermostat broadcast giving us temps +#define EMS_TYPE_RC35StatusMessage_setpoint 2 // desired temp +#define EMS_TYPE_RC35StatusMessage_curr 3 // current temp +#define EMS_TYPE_RC35Set 0x3D // for setting values like temp and mode (Working mode HC1) +#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode +#define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time +#define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time +#define EMS_OFFSET_RC35Get_mode_day 1 // position of thermostat day mode + +// Easy specific +#define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat +#define EMS_TYPE_EasyStatusMessage_setpoint 10 // setpoint temp +#define EMS_TYPE_EasyStatusMessage_curr 8 // current temp + +// Known EMS types +typedef enum { + EMS_MODEL_NONE, + EMS_MODEL_ALL, // common for all devices + + // generic ID for the boiler + EMS_MODEL_UBA, + + // thermostats + EMS_MODEL_ES73, + EMS_MODEL_RC10, + EMS_MODEL_RC20, + EMS_MODEL_RC20F, + EMS_MODEL_RC30, + EMS_MODEL_RC35, + EMS_MODEL_EASY, + EMS_MODEL_BOSCHEASY, + EMS_MODEL_RC310, + EMS_MODEL_CW100, + EMS_MODEL_OT + +} _EMS_MODEL_ID; + +// EMS types for known Buderus/Bosch devices. This list will be extended when new devices are recognized. +// format is MODEL_ID, PRODUCT ID, TYPE_ID, DESCRIPTION +const _Boiler_Type Boiler_Types[] = { + + {EMS_MODEL_UBA, 72, 0x08, "MC10"}, + {EMS_MODEL_UBA, 123, 0x08, "Buderus GB172/Nefit Trendline"}, + {EMS_MODEL_UBA, 115, 0x08, "Nefit Topline Compact"}, + {EMS_MODEL_UBA, 203, 0x08, "Buderus Logamax U122"}, + {EMS_MODEL_UBA, 64, 0x08, "Sieger BK15 Boiler/Nefit Smartline"}, + {EMS_MODEL_UBA, 190, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_UBA, 114, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_UBA, 125, 0x09, "BC25 Base Controller"}, + {EMS_MODEL_UBA, 68, 0x09, "RFM20 Receiver"}, + {EMS_MODEL_UBA, 95, 0x08, "Bosch Condens 2500"}, + {EMS_MODEL_UBA, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! + {EMS_MODEL_UBA, 250, 0x11, "WM10 Switch Module"}, // warning, fake product id! + +}; + +/* + * Known thermostat types and their capabilities + */ +const _Thermostat_Type Thermostat_Types[] = { + + {EMS_MODEL_ES73, 76, 0x10, "Sieger ES73", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC10, 79, 0x17, "RC10/Nefit Moduline 100)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC20, 77, 0x17, "RC20/Nefit Moduline 300)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC20F, 93, 0x18, "RC20F", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC30, 78, 0x10, "RC30/Nefit Moduline 400)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC35, 86, 0x10, "RC35", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_EASY, 202, 0x18, "TC100/Nefit Easy", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_BOSCHEASY, 206, 0x02, "Bosch Easy", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_RC310, 158, 0x10, "RC310", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_CW100, 255, 0x18, "Bosch CW100", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES} + +}; diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 0724b29db..454ecb0fd 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -2,7 +2,7 @@ * emsuart.cpp * * The low level UART code for ESP8266 to read and write to the EMS bus via uart - * Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler + * Paul Derbyshire - https://github.com/proddy/EMS-ESP */ #include "emsuart.h" @@ -28,8 +28,8 @@ static void emsuart_rx_intr_handler(void * para) { static uint8_t uart_buffer[EMS_MAXBUFFERSIZE]; // is a new buffer? if so init the thing for a new telegram - if (EMS_Sys_Status.emsRxStatus == EMS_RX_IDLE) { - EMS_Sys_Status.emsRxStatus = EMS_RX_ACTIVE; // status set to active + if (EMS_Sys_Status.emsRxStatus == EMS_RX_STATUS_IDLE) { + EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_BUSY; // status set to busy length = 0; } @@ -55,7 +55,7 @@ static void emsuart_rx_intr_handler(void * para) { os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, length); // set the status flag stating BRK has been received and we can start a new package - EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE; + EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // call emsuart_recvTask() at next opportunity system_os_post(EMSUART_recvTaskPrio, 0, 0); @@ -68,6 +68,7 @@ static void emsuart_rx_intr_handler(void * para) { /* * system task triggered on BRK interrupt * Read commands are all asynchronous + * When a buffer is full it is sent to the ems_parseTelegram() function in ems.cpp. This is the hook */ static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) { // get next free EMS Receive buffer @@ -109,7 +110,7 @@ void ICACHE_FLASH_ATTR emsuart_init() { USC0(EMSUART_UART) |= (tmp); // set bits USC0(EMSUART_UART) &= ~(tmp); // clear bits - // conf 1 params + // conf1 params // UCTOE = RX TimeOut enable (default is 1) // UCTOT = RX TimeOut Threshold (7bit) = want this when no more data after 2 characters. (default is 2) // UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer. (default was 127). @@ -137,6 +138,14 @@ void ICACHE_FLASH_ATTR emsuart_init() { system_uart_swap(); } +/* + * stop UART0 driver + */ +void ICACHE_FLASH_ATTR emsuart_stop() { + ETS_UART_INTR_DISABLE(); + ETS_UART_INTR_ATTACH(NULL, NULL); +} + /* * Send a BRK signal * Which is a 11-bit set of zero's (11 cycles) diff --git a/src/emsuart.h b/src/emsuart.h index f3d3f6739..f5053bda3 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -1,13 +1,15 @@ /* * emsuart.h + * * Header file for emsuart.cpp - * Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP */ #pragma once #include -#define EMSUART_UART 0 // UART 0 - there is only one on the esp8266 +#define EMSUART_UART 0 // UART 0 #define EMSUART_CONFIG 0x1c // 8N1 (8 bits, no stop bits, 1 parity) #define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit @@ -23,11 +25,12 @@ #define EMSUART_recvTaskQueueLen 64 typedef struct { - int16_t writePtr; + uint8_t writePtr; uint8_t buffer[EMS_MAXBUFFERSIZE]; } _EMSRxBuf; void ICACHE_FLASH_ATTR emsuart_init(); +void ICACHE_FLASH_ATTR emsuart_stop(); void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len); void ICACHE_FLASH_ATTR emsaurt_tx_poll(); void ICACHE_FLASH_ATTR emsuart_tx_brk(); diff --git a/src/my_config.h b/src/my_config.h index d2a1a4840..2c23d611b 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -1,48 +1,71 @@ /* * my_config.h + * * All configurations and customization's go here - * - * Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP */ #pragma once -// these are set as -D build flags during compilation -// they can be set in platformio.ini or alternatively hard coded here +#include "ems.h" -// WIFI settings -//#define WIFI_SSID "" -//#define WIFI_PASSWORD "" +// MQTT base name +#define MQTT_BASE "home" // all MQTT topics are prefix with this string, in the format // -// MQTT settings -// Note port is the default 1883 -//#define MQTT_IP "" -//#define MQTT_USER "" -//#define MQTT_PASS "" +// MQTT general settings +#define MQTT_TOPIC_START "start" +#define MQTT_TOPIC_START_PAYLOAD "start" +#define MQTT_WILL_TOPIC "status" // for last will & testament topic name +#define MQTT_WILL_ONLINE_PAYLOAD "online" // for last will & testament payload +#define MQTT_WILL_OFFLINE_PAYLOAD "offline" // for last will & testament payload +#define MQTT_RETAIN false +#define MQTT_KEEPALIVE 120 // 2 minutes +#define MQTT_QOS 1 -// 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 +// MQTT for thermostat +#define TOPIC_THERMOSTAT_DATA "thermostat_data" // for sending thermostat values to MQTT +#define TOPIC_THERMOSTAT_CMD_TEMP "thermostat_cmd_temp" // for received thermostat temp changes via MQTT +#define TOPIC_THERMOSTAT_CMD_MODE "thermostat_cmd_mode" // for received thermostat mode changes via MQTT +#define THERMOSTAT_CURRTEMP "thermostat_currtemp" // current temperature +#define THERMOSTAT_SELTEMP "thermostat_seltemp" // selected temperature +#define THERMOSTAT_MODE "thermostat_mode" // mode -// 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 +// MQTT for boiler +#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT +#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running +#define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on +#define TOPIC_BOILER_WWACTIVATED "wwactivated" // for receiving MQTT message to change water on/off +#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT -// 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 +// shower time +#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 an MQTT command -// 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 -#define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water -#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 +// default values for shower logic on/off +#define BOILER_SHOWER_TIMER 1 // enable (1) to monitor shower time +#define BOILER_SHOWER_ALERT 0 // enable (1) to send alert of cold water when shower time limit has exceeded +#define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water -// 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 +//////////////////////////////////////////////////////////////////////////////////////////////////// +// THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // +// ALTHOUGH YOU MAY ALSO HARDCODE THEM HERE BUT THEY WILL BE OVERWRITTEN WITH NEW RELEASE UPDATES // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// Set LED pin used for showing ems bus connection status. Solid is connected, Flashing is error +// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED +// (e.g. D1 on a bbqkees' board +// can be enabled and disabled via the 'set led' +// pin can be set by 'set led_gpio' +#define EMSESP_LED_GPIO LED_BUILTIN + +// set this if using an external temperature sensor like a DS18B20 +// D5 is the default on bbqkees' board +#define EMSESP_DALLAS_GPIO D5 + +// By default the EMS bus will be scanned for known devices based on product ids in ems_devices.h +// You can override the Thermostat and Boiler types here +#define EMSESP_BOILER_TYPE EMS_ID_NONE +#define EMSESP_THERMOSTAT_TYPE EMS_ID_NONE diff --git a/src/version.h b/src/version.h index c65eb0bbe..0eed2b101 100644 --- a/src/version.h +++ b/src/version.h @@ -1,2 +1,10 @@ -#define APP_NAME "EMS-ESP-Boiler" -#define APP_VERSION "1.1.0" +/** + * + * Paul Derbyshire - https://github.com/proddy/EMS-ESP + */ + +#pragma once + +#define APP_NAME "EMS-ESP" +#define APP_VERSION "1.5.7b" +#define APP_HOSTNAME "ems-esp"