This commit is contained in:
proddy
2019-03-10 15:09:51 +01:00
parent b52794998e
commit 8e62a99417
46 changed files with 6145 additions and 5523 deletions

View File

@@ -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 <ssid> <password>` 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)

View File

@@ -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 <COPYRIGHT HOLDER> ''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 <COPYRIGHT HOLDER> 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).

339
README.md
View File

@@ -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 `<BRK>`, 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 `<BRK>`, 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] <BRK>``
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] <BRK>`.
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] <BRK>` (meaning us) and then send back `[0x0B] <BRK>` 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] <BRK>` (meaning us) and then send back `[0x0B] <BRK>` 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 (`<ID> <BRK>`) 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 (`<ID> <BRK>`) 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 |
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 <com> write_flash 0x00000 <firmware>` where firmware is the `.bin` file and \<com\> 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 "<my_ssid>"
#define WIFI_PASSWORD "<my_password>"
#define MQTT_IP "<broker_ip>"
#define MQTT_USER "<broker_username>"
#define MQTT_PASS "<broker_password>"
```
## 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 <com port> write_flash 0x00000 <firmware.bin file>`. 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

28
checkcode.py Normal file
View File

@@ -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'])

24
clean_fw.py Normal file
View File

@@ -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'])

113
doc/Domoticz/nefit/mqtt.py Normal file
View File

@@ -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'])

View File

@@ -0,0 +1,165 @@
"""
<plugin key="nefit" name="Nefit EMS-ESP with Proddy firmware" version="0.0.1">
<description>
Plugin to control Nefit EMS-ESP with '<a href="https://github.com/proddy/EMS-ESP"> Proddy</a>' firmware<br/>
<br/>
Automatically creates Domoticz devices for connected device.<br/>
Do not forget to "Accept new Hardware Devices" on first run<br/>
</description>
<params>
<param field="Address" label="MQTT Server address" width="300px" required="true" default="127.0.0.1"/>
<param field="Port" label="Port" width="300px" required="true" default="1883"/>
<param field="Mode6" label="Debug" width="75px">
<options>
<option label="Extra verbose" value="Verbose+"/>
<option label="Verbose" value="Verbose"/>
<option label="True" value="Debug"/>
<option label="False" value="Normal" default="true" />
</options>
</param>
</params>
</plugin>
"""
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()

10
doc/Domoticz/readme.txt Normal file
View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -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") }}

View File

@@ -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"

View File

@@ -5,12 +5,12 @@
- 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 }}"
@@ -18,3 +18,13 @@
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 }}"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -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'

View File

@@ -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") }}'

View File

@@ -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

View File

@@ -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

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

BIN
doc/telnet/telnet_menu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1,466 +0,0 @@
// Boiler
// Espurna version
// Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler
#include "emsuart.h"
#include <ArduinoJson.h>
#include <ems.h>
#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
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

1178
lib/MyESP/MyESP.cpp Normal file

File diff suppressed because it is too large Load Diff

228
lib/MyESP/MyESP.h Normal file
View File

@@ -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 <ArduinoJson.h>
#include <ArduinoOTA.h>
#include <AsyncMqttClient.h> // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127
#include <DNSServer.h>
#include <FS.h>
#include <JustWifi.h> // https://github.com/xoseperez/justwifi
#include <TelnetSpy.h> // modified from https://github.com/yasheena/telnetspy
#if defined(ARDUINO_ARCH_ESP32)
//#include <ESPmDNS.h>
#include <SPIFFS.h> // added for ESP32
#define ets_vsnprintf vsnprintf // added for ESP32
#define OTA_PORT 8266
#else
//#include <ESP8266mDNS.h>
#include <ESPAsyncTCP.h>
#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<void(unsigned int, const char *, const char *)> mqtt_callback_f;
typedef std::function<void()> wifi_callback_f;
typedef std::function<void()> ota_callback_f;
typedef std::function<void(uint8_t, const char *)> telnetcommand_callback_f;
typedef std::function<void(uint8_t)> telnet_callback_f;
typedef std::function<bool(MYESP_FSACTION, const JsonObject json)> fs_callback_f;
typedef std::function<bool(MYESP_FSACTION, uint8_t, const char *, const char *)> fs_settings_callback_f;
// calculates size of an 2d array at compile time
template <typename T, size_t N>
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

655
lib/TelnetSpy/TelnetSpy.cpp Normal file
View File

@@ -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();
}
}
}

281
lib/TelnetSpy/TelnetSpy.h Normal file
View File

@@ -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.h>
* 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 <minSize> (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 <ESP8266WiFi.h>
#else // ESP32
#include <WiFi.h>
#endif
#include <WiFiClient.h>
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<void()> 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

View File

@@ -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

13
rename_fw.py Normal file
View File

@@ -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'])

View File

@@ -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("<unknown>");
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 <hostname> or on Windows10 it's <hostname>.
#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/<hostname>/command/<cmd>
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/<hostname>/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);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <ArduinoOTA.h>
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
#include <ESP8266mDNS.h>
#include <Print.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
#include <pgmspace.h>
// 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<void(char *, uint8_t *, uint8_t)> _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];
};

View File

@@ -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 <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson
#include <CRC32.h> // https://github.com/bakercp/CRC32
// standard arduino libs
#include <Ticker.h> // 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/<hostname>)
#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 <typename T, size_t N>
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("<not supported>\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
}

221
src/ds18.cpp Normal file
View File

@@ -0,0 +1,221 @@
/*
* Dallas support for external settings
* Copied from Espurna - Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
* See ChangeLog.md for history
* See README.md for Acknowledgments
*
*/
#include "ds18.h"
std::vector<ds_device_t> _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());
}

55
src/ds18.h Normal file
View File

@@ -0,0 +1,55 @@
/*
* Dallas support for external temperature sensors
* Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
* See ChangeLog.md for history
* See README.md for Acknowledgments
*
*/
#pragma once
#include <OneWire.h>
#include <vector>
#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
};

1211
src/ems-esp.ino Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

209
src/ems.h
View File

@@ -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
@@ -10,82 +14,52 @@
// 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_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_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_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
@@ -111,16 +85,18 @@ typedef struct {
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
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,6 +155,7 @@ 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
@@ -175,33 +171,53 @@ typedef struct { // UBAParameterWW
uint8_t curBurnPow; // Burner current power
float flameCurr; // Flame current in micro amps
float sysPress; // System pressure
char serviceCodeChar[2]; // 2 character status/service code
// 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_scanDevices();
void ems_printAllTypes();
void ems_printThermostatType();
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;

146
src/ems_devices.h Normal file
View File

@@ -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}
};

View File

@@ -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)

View File

@@ -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 <Arduino.h>
#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();

View File

@@ -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 "<my_ssid>"
//#define WIFI_PASSWORD "<my_password>"
// MQTT base name
#define MQTT_BASE "home" // all MQTT topics are prefix with this string, in the format <MQTT_BASE>/<app name>/<topic>
// MQTT settings
// Note port is the default 1883
//#define MQTT_IP "<broker_ip>"
//#define MQTT_USER "<broker_username>"
//#define MQTT_PASS "<broker_password>"
// 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
// 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
#define SHOWER_OFFSET_TIME 5000 // in ms. 5 seconds grace time, to calibrate actual time under the shower
#define SHOWER_COLDSHOT_DURATION 10 // in seconds. 10 seconds for cold water before turning back hot water
// if using LEDs to show traffic, configure the GPIOs here
// only works if -DUSE_LED is set in platformio.ini
#define LED_RX D1 // GPIO5
#define LED_TX D2 // GPIO4
#define LED_ERR D3 // GPIO0
////////////////////////////////////////////////////////////////////////////////////////////////////
// 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

View File

@@ -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"