diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7da6aa0cc..b4f83fdf6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,7 +10,7 @@ assignees: '' *Before creating a new issue please check that you have:* * *searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* -* *searched the [wiki help pages](https://github.com/proddy/EMS-ESP/wiki/Troubleshooting)* +* *searched the [wiki help pages](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)* *Completing this template will help developers and contributors to address the issue. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.* diff --git a/.github/ISSUE_TEMPLATE/questions---troubleshooting.md b/.github/ISSUE_TEMPLATE/questions---troubleshooting.md index 451aa0cb0..d78997967 100644 --- a/.github/ISSUE_TEMPLATE/questions---troubleshooting.md +++ b/.github/ISSUE_TEMPLATE/questions---troubleshooting.md @@ -10,8 +10,7 @@ assignees: '' *Before creating a new issue please check that you have:* * *searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* -* *searched the [wiki help pages](https://github.com/proddy/EMS-ESP/wiki/Troubleshooting)* - +* *searched the [wiki help pages](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)* *Completing this template will help developers and contributors help you. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.* diff --git a/.github/contribute.md b/.github/contribute.md deleted file mode 100644 index a81ee132a..000000000 --- a/.github/contribute.md +++ /dev/null @@ -1,10 +0,0 @@ -Do you want to do a pull request? - -Excellent! Thanks for contributing! - -Please do keep in mind these basic rules: - -## Pull request ## -* Do the pull request against the **`dev` branch** -* **Only touch relevant files** (beware if your editor has auto-formatting feature enabled) - diff --git a/.github/stale.yml b/.github/stale.yml index 046b9c15a..608af9185 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,15 +1,16 @@ # Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 +daysUntilStale: 40 # Number of days of inactivity before a stale Issue or Pull Request is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 7 +daysUntilClose: 5 # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: + - pinned + - security - enhancement - bug - - staged for release # Set to true to ignore issues in a project (defaults to false) exemptProjects: false @@ -23,19 +24,17 @@ staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had - recent activity. It will be closed in 7 days if no further activity occurs. - Thank you for your contributions. - + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. # Comment to post when removing the stale label. # unmarkComment: > # Your comment here. # Comment to post when closing a stale Issue or Pull Request. closeComment: > - This issue will be auto-closed because there hasn't been any activity for two months. Feel free to open a new one if you still experience this problem. - + This issue will be auto-closed because there hasn't been any activity for a few months. Feel free to open a new one if you still experience this problem. # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 # Limit to only `issues` or `pulls` -only: issues +#only: issues \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6381205a1..31d9e3233 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -60,3 +60,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 + diff --git a/.gitignore b/.gitignore index 8bd2747ba..b85d4d549 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ pio_local.ini # project specfic /scripts/stackdmp.txt -*.bin emsesp /data/www/ /lib/framework/WWWData.h diff --git a/.travis.yml b/.travis.yml index 7c8b588c8..6aa7c8e90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,8 @@ env: global: - BUILDER_TOTAL_THREADS=1 - OWNER=${TRAVIS_REPO_SLUG%/*} - - DEV=${OWNER/proddy/v2} - - BRANCH=${TRAVIS_BRANCH/v2/} + - DEV=${OWNER/proddy/dev} + - BRANCH=${TRAVIS_BRANCH/dev/} - TAG=${DEV}${BRANCH:+_}${BRANCH} install: @@ -53,7 +53,7 @@ deploy: token: ${GITHUB_TOKEN} file_glob: true file: "*.bin" - name: latest v2 development build + name: latest development build release_notes: Version $FIRMWARE_VERSION. Automatic firmware build of the current EMS-ESP branch built on $(date +'%F %T %Z') from commit $TRAVIS_COMMIT. diff --git a/CHANGELOG.md b/CHANGELOG.md index 943ee9286..e68eb1cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,51 @@ 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). -## [2.0.0 beta] +## [2.0.1] + +### Added +- Able to set individual MQTT publish intervals per device +- Option to automatically MQTT publish when device data is updated +- Immediately send out Rx read request after a successful write, and publish via MQTT +- Added clearer steps in documentation on how to erase & upload +- Show Boiler's pump modulation in Web +- Support parasite Dallas temperature sensors +- Improvements to `watch` command, including publishing the telegram to MQTT +- Support for analog measurements on a GPIO (fixed) +- New `read ` command in console + +### Fixed +- Sometimes the automatic upgrade from 1.9 to 2.0 bricked the ESP8266 +- Thermostat `set master` wasn't preserved after restart +- Correctly detect Thermostat heating circuits in Home Assistant +- Logamatic TC100 reading of thermostat data (and other Easy devices) +- Rendering 3-byte parameters like the UBA uptime +- MM100/200 MQTT data would be mixed up between heating circuit and ww circuit +- External Dallas sensor support for DS18S20 ### Changed -- A lot! See `README.md` +- Web user-interface improvements, table alignment and number formatting +- Spelling of disinfection in MQTT payload +- Many small minor code improvements and optimizations +- External dallas temperature sensors rounded to a single decimal point +- Syslog hostname always shown in Web + +### Removed +- NO_LED build option + +## [2.0.0] 29-08-2020 + +First version of v2 with +- Supporting both ESP8266 and ESP32 modules from Espressif +- A new multi-user Web interface (based on React/TypeScript) +- A new Console, accessible via Serial and Telnet +- Tighter security in both Web and Console. Admin privileges required to access core settings and commands. +- Support for Home Assistant MQTT Discovery (https://www.home-assistant.io/docs/mqtt/discovery/) +- Can be run standalone as an independent Access Point or join an existing WiFi network +- Easier first-time configuration via a web Captive Portal +- Supporting over 70 EMS devices (boilers, thermostats, solar modules, mixing modules, heat pumps, gateways) + +See README.me for more details. ## [1.9.5] 30-04-2020 @@ -38,7 +79,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - improved MQTT publishing to stop network flooding. `publish_time` of -1 is no publish, 0 is automatic otherwise its a time interval -- External sensors (like Dallas DS18*) are sent as a nested MQTT topic including their unqiue identifier +- External sensors (like Dallas DS18*) are sent as a nested MQTT topic including their unique identifier - `mqttlog` console command renamed to `mqttqueue` to only show the current publish queue - `status` payload on start-up shows the IP and Version of EMS-ESP - `thermostat mode` takes a string like manual,auto,heat,day,night,eco,comfort,holiday,nofrost @@ -160,7 +201,7 @@ There are breaking changes in this release. See `publish_time` below and make su - Fixes to the default HA climate component .yaml file to support latest Home Assistance ('heat' added) - Update documentation in Wiki on MQTT and troubleshooting - Slowed down firmware upload via the Web to prevent users rebooting too early -- Change way WiFi is intialized to prevent dual AP and Client +- Change way WiFi is initialized to prevent dual AP and Client ### Removed @@ -210,7 +251,7 @@ There are breaking changes in this release. See `publish_time` below and make su - Stopped automatic refresh of web page, which causes crashes/memory loss after a short time - Support HA 0.96 climate component changes - -DDEFAULT_NO_SERIAL changed to -DFORCE_SERIAL -- some code cleanups, removing NULLS and moving some things fron heap to stack to prevent memory fragmentation +- some code cleanups, removing NULLS and moving some things frond heap to stack to prevent memory fragmentation ## [1.8.0] 2019-06-15 @@ -455,7 +496,7 @@ There are breaking changes in this release. See `publish_time` below and make su - 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 +- pre-built firmware's are back ## [1.2.4] 2019-01-04 @@ -487,7 +528,7 @@ There are breaking changes in this release. See `publish_time` below and make su - 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) +- Removed distinguishing between noise on the line and corrupt telegrams (https://github.com/proddy/EMS-ESP/issues/24) ## [1.2.0] 2019-01-01 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..b357c9285 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,110 @@ +Logo + + +# Contributing + +**Any contribution helps EMS-ESP get better for the entire community!** + +Everybody is welcome and invited to contribute to the EMS-ESP Project by: + +- providing Pull Requests (Features, Fixes, suggestions) +- testing new released features and report issues on your EMS equipment +- contributing missing [documentation](https://emsesp.github.io/docs) for features and devices + +This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs. + +## Opening New Issues + +1. Opening an issue means that a problem exists in the code and should be addressed by the project contributors. +2. When opening an issue, it is required to fill out the presented template. The requested information is important! If the template is ignored or insufficient info about the issue is provided, the issue may be closed. +3. Questions of type "How do I..." or "Can you please help me with..." or "Can EMS-ESP do..." are better directed to the support channel in Gitter. +4. Issues about topics already handled in the documentation will be closed in a similar manner. +5. Issues for unmerged PRs will be closed. If there is an issue with a PR, the explanation should be added to the PR itself. +6. Issues with accompanied investigation that shows the root of the problem should be given priority. +7. Duplicate issues will be closed. + +## Triaging of Issues/PR's + +1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so. +2. An issue that needs to be closed, either due to not complying with this policy, or for other reasons, should be closed by a contributor. +3. Issues that are accepted should be marked with appropriate labels. +4. Issues that could impact functionality for many users should be considered severe. +5. Issues caused by the SDK or chip should not be marked severe, as there usually isn’t much to be done. Common sense should be applied when deciding. Such issues should be documented in the Wiki, for reference by users. +6. Issues with feature requests should be discussed for viability/desirability. +7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified. +8. Feature requests that are not accompanied by a PR: + * could be closed immediately (denied). + * could be closed after some predetermined period of time (left as candidate for somebody to pick up). +9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot. + +## Pull requests + +A Pull Request (PR) is the process where code modifications are managed in GitHub. + +The process is straight-forward. + + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) + - Fork the EMS-ESP Repository [git repository](https://github.com/proddy/EMS-ESP). + - Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc. + - Ensure tests work. + - Create a Pull Request against the [**dev**](https://github.com/proddy/EMS-ESP/tree/dev) branch of EMS-ESP. + +1. All pull requests must be done against the dev branch. +2. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled). +3. Only one feature/fix should be added per PR. +4. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts. +5. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner. +6. All pull requests should consider updates to the documentation. +7. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority. +8. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged. +9. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA. +10. Pull requests that don't meet the above will be denied and closed. + +-------------------------------------- + +## Contributor License Agreement (CLA) + +``` +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the GPL-3.0 license; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the GPL-3.0 license; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it) is maintained indefinitely + and may be redistributed consistent with this project or the open + source license(s) involved. +``` + +This Contributor License Agreement (CLA) was adopted on April 1st, 2019. + +The text of this license is available under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-sa/3.0/). It is based on the Linux [Developer Certificate Of Origin](http://elinux.org/Developer_Certificate_Of_Origin), but is modified to explicitly use the GPL-3.0 license and not mention sign-off (due to GitHub.com keeps an historial, with your user name, of PRs' commits and all editions on PR's comments). + +To accept the CLA it is required to put a x between [ ] on `[ ] I accept the CLA` in the PR template when submitting it. The [ ] is an opt-in box, so you have to manually accept it. + +**Why a CLA ?** + +_"A Contributor Licence Agreement (CLA) is strongly recommended when accepting third party contributions to an open development project, such as an open source software project. In order to redistribute contributions, it is necessary to ensure that the project has the necessary rights to do so. A Contributor Licence Agreement is a lightweight agreement, signed by the copyright holder, that grants the necessary rights for the contribution to be redistributed as part of the project."_ [OSS Watch](http://oss-watch.ac.uk/resources/cla) + +A CLA is a legal document in which you state _you are entitled to contribute the code/documentation/translation to the project_ you’re contributing to and that _you are willing to have it used in distributions and derivative works_. This means that should there be any kind of legal issue in the future as to the origins and ownership of any particular piece of code, then that project has the necessary forms on file from the contributor(s) saying they were permitted to make this contribution. + +CLA is a safety because it also ensures that once you have provided a contribution, you cannot try to withdraw permission for its use at a later date. People can therefore use that software, confident that they will not be asked to stop using pieces of the code at a later date. + +A __license__ grants "outbound" rights to the user of project. + +A __CLA__ enables a contributor to grant "inbound" rights to a project. + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4956c3287..951b87731 100644 --- a/README.md +++ b/README.md @@ -1,188 +1,171 @@ # ![logo](media/EMS-ESP_logo_dark.png) -[![version](https://img.shields.io/github/release/proddy/EMS-ESP.svg?label=Latest%20Release)](https://github.com/proddy/EMS-ESP/blob/main/CHANGELOG.md) -[![release-date](https://img.shields.io/github/release-date/proddy/EMS-ESP.svg?label=Released)](https://github.com/proddy/EMS-ESP/commits/main) -
+**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger. + +[![version](https://img.shields.io/github/release/proddy/EMS-ESP.svg?label=Latest%20Release)](https://github.com/proddy/EMS-ESP/blob/master/CHANGELOG.md) +[![release-date](https://img.shields.io/github/release-date/proddy/EMS-ESP.svg?label=Released)](https://github.com/proddy/EMS-ESP/commits/master) [![license](https://img.shields.io/github/license/proddy/EMS-ESP.svg)](LICENSE) [![travis](https://travis-ci.com/proddy/EMS-ESP.svg?branch=dev)](https://travis-ci.com/proddy/EMS-ESP) [![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) [![downloads](https://img.shields.io/github/downloads/proddy/EMS-ESP/total.svg)](https://github.com/proddy/EMS-ESP/releases) -
+[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/proddy/EMS-ESP.svg)](http://isitmaintained.com/project/proddy/EMS-ESP "Average time to resolve an issue") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/proddy/EMS-ESP.svg)](http://isitmaintained.com/project/proddy/EMS-ESP "Percentage of issues still open") +
[![gitter](https://img.shields.io/gitter/room/EMS-ESP/EMS-ESP.svg)](https://gitter.im/EMS-ESP/community) -
-EMS-ESP is a open-source system built for the Espressif ESP8266 microcontroller to communicate with **EMS** (Energy Management System) based boilers, thermostats and other modules from manufacturers like Bosch, Buderus, Nefit, Junkers and Sieger. +If you like **EMS-ESP**, please give it a star, or fork it and contribute! -## **New Features in v2** +[![GitHub stars](https://img.shields.io/github/stars/proddy/EMS-ESP.svg?style=social&label=Star)](https://github.com/proddy/EMS-ESP/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/proddy/EMS-ESP.svg?style=social&label=Fork)](https://github.com/proddy/EMS-ESP/network) +[![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/paypalme/prderbyshire/2) -- Supports both ESP8266 and ESP32 -- New MQTT option to support Home Assistant MQTT Discovery (https://www.home-assistant.io/docs/mqtt/discovery/) -- Tighter security in Web and Console -- New secure web interface (based on React/TypeScript) -- Can be run on WiFi on as a Stand alone Access Point +Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be purchased at https://bbqkees-electronics.nl/. + + + +--- + +## **New Features in version 2** + +- Support for both ESP8266 and ESP32 modules +- A new multi-user Web interface (based on React/TypeScript) +- A new Console, accessible via Serial and Telnet +- Tighter security in both Web and Console. Admin privileges required to access core settings and commands. +- Support for Home Assistant MQTT Discovery (https://www.home-assistant.io/docs/mqtt/discovery/) +- Can be run standalone as an independent Access Point or join an existing WiFi network - Easier first-time configuration via a web Captive Portal -- Supporting over 70 EMS devices - - - - - - -- A new console. As in version 1.9 it works with both Serial and Telnet but now with a more intuitive Linux shell like behavior. It supports multiple connections and has basic security to prevent any changes to EMS-ESP. A full list of commands is below, here are the key ones: - * `help` lists the commands and keywords. This works in each context. - * `exit` will exit the console or exit the current context. CTRL-D does the same. - * `CTRL-U` for Undo - * `TAB` for auto-complete - * Some specific commands are behind contexts. Think of this as a sub-menu. e.g. `system`, `thermostat`. The path will always show you which context you are in. `$` is the root. - * `su` will switch to super-user or Admin. The default password is "ems-esp-neo" and can be changed with `passwd` from the system menu or via the Web UI (called secret password). When in Admin mode the command prompt switches from `$` to `#`. - * Some settings can be changed in the console. The `set` command will list them. - * `show` shows the data specific to the which context you're in. From the root it will show you all the EMS device information and any external temperature sensors. From a context it will be more specific to that context, e.g. `show mqtt` from `system` will list MQTT subscriptions and show the status and queue. - * `log` sets the logging level. `log off` disables logging. Use `log debug` for debugging commands and actions. This will be reset next time the console is opened. - * `watch` will output the incoming Rx telegrams directly to the console. You can also put on a watch on a specific EMS device ID or telegram ID. Also choose to output as verbose text or raw data bytes. these in its 'raw' data format and also watch a particular ID. +- Supporting over 70 EMS devices (boilers, thermostats, solar modules, mixing modules, heat pumps, gateways) -## **Migrating from version 1.9** +## **Screenshots** + +| | | +| --- | --- | +| | | +| | | + + +## **Migrating from versions 1.9** EMS-ESP will attempt to automatically migrate the 1.9 settings. -Note there are some noticeable different to be aware of in version 2: - - MQTT base has been removed - - There is no "serial mode" anymore like with version 1.9. When the Wifi cannot connect to the SSID it will automatically enter a "safe" mode where the Serial console is automatically activated (note Serial is always available on the ESP32 because it has multiple UARTs). The EMS-ESP will blink fast when in Serial mode. Connect via a USB with baud 115200 to see the serial console. Note in this mode the EMS will be disconnect so there will be no incoming traffic. Use only for debugging or changing settings. +Note there are some noticeable differences to be aware of in version 2: +### MQTT: + - MQTT base has been removed. All MQTT topics are prefixed with only the hostname, for example `ems-esp/status` as opposed to `home/ems-esp/status`. + - `heatPmp` renamed to `heatPump` + - `ServiceCodeNumber` renamed to `serviceCodeNumber` + - Firmware version has been moved to the `start` topic + - `desinfection` renamed to `disinfection` +### General: + - There is no "serial mode" anymore like with version 1.9. When the Wifi cannot connect to the SSID it will automatically enter a "safe" mode where the Serial console is activated (note Serial is always available on the ESP32 because it has multiple UARTs). The EMS-ESP's LED will blink fast when in Serial mode. When this happens connect via a USB using baud 115200. + +If you run into issues try first erasing the ESP8266 with `esptool.py erase_flash` and uploading the new firmware manually. BBQKees has a good write-up at https://bbqkees-electronics.nl/wiki/gateway/firmware-update-to-v2.html. + +## **Building the firmware using PlatformIO** + +1. Install [PlatformIO](https://platformio.org/install) and [NodeJS](https://nodejs.org/en/). +2. Decide how you want to upload the firmware, via USB or OTA (Over The Air). OTA requires that a version of EMS-ESP is already running. +3. Create a new file called `pio_local.ini` and add these two lines for USB: +```yaml +upload_protocol = esptool +upload_port = +``` +or these 2 for OTA: +```yaml +upload_protocol = espota +upload_flags = + --port=8266 + --auth=ems-esp-neo +upload_port = ems-esp.local +``` +3. type `pio run -t upload` to build and upload the firmware ## **Uploading the firmware** -- If you're not using PlatformIO, use the command-line and Python. You can download Python from https://www.python.org/downloads/. Make sure you also get: - - `esptool`, install using the command `pip install esptool` - - and for OTA updates later, `espota` from https://github.com/esp8266/Arduino/blob/master/tools/espota.py using `python espota.py --debug --progress --port 8266 --auth ems-esp-neo -i ems-esp.local -f ` +Here we'll use the command-line. You'll need [Python]( https://www.python.org/downloads/) (version 3) installed and these 2 scripts: -- Grab the latest firmware binary from https://github.com/proddy/EMS-ESP/releases +- `esptool.py`. Install using `pip install esptool`. +- `espota.py` downloaded from https://github.com/esp8266/Arduino/blob/master/tools/espota.py -- Uploading directly via USB... +Both these tools are also in the repo in the `scripts` directory. + +Next step is to fetch the latest firmware binary from https://github.com/proddy/EMS-ESP/releases, and if you're using USB with an ESP8266: + + `esptool.py -p -b 921600 write_flash 0x00000 ` - For ESP8266: `esptool.py -p -b 921600 write_flash 0x00000 ` - note: if this fails try a lower speed like `115200` instead of `921600`. +and for OTA: - For ESP32: `esptool.py --chip esp32 --port "COM6" --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 XX\.platformio\packages\framework-arduinoespressif32\tools\sdk\bin\bootloader_dio_40m.bin 0x8000 XX\.pio\build\esp32\partitions.bin 0xe000 XX\.platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin 0x10000 ` -- Uploading over WiFi: `espota.py --debug --progress --port 8266 --auth ems-esp-neo -i -f ` + `espota.py --debug --progress --port 8266 --auth ems-esp-neo -i -f ` -## **Setting EMS-ESP up for the first time** +## **Configuring EMS-ESP for the first time** - - After powering up the ESP, watch the onboard LED. A solid light means good connection and EMS data is coming in. A slow pulse means either the WiFi or the EMS bus is not connected yet. A very fast pulse is when the system is booting up and configuring itself, which typically takes 5 seconds. + - After powering up the ESP, watch the onboard blue LED. A solid light means good connection and EMS data is coming in. A slow pulse means either the WiFi or the EMS bus is not connected yet. A very fast pulse is when the system is booting up and configuring itself which typically takes 5 seconds. - - Connect to the Access Point called ems-esp using the WPA password `ems-esp-neo`. When you see the captive portal sign-in with username `admin` and password `admin`. Set the WiFi credentials and go back to http://ems-esp + - Connect to the Access Point called `ems-esp` using the WPA password `ems-esp-neo`. When you see the captive portal sign-in with username `admin` and password `admin`. Set the WiFi credentials and go back to http://ems-esp. Remember to change the passwords! - - First thing to check is if Tx is working and that you have a connect to the EMS bus. If it's showing an error try changing the Tx Mode from the settings page. Then check the Status (no need to restart EMS-ESP). + - First thing to check is if Tx is working and that you have a connection to the EMS bus. If Tx fails are shown in the Web interface try changing the Tx Mode from the settings page. There is no need to re-start the EMS-ESP. - - If Rx incomplete telegrams are reported in the Web UI, don't panic. Some telegrams can be missed and this is usually due to noise on line. + - If Rx incomplete telegrams are reported in the Web interface, don't panic. Some telegrams can be missed and this is usually caused by noise interference on the line. -## **Using the Console** +## **Using the Console** -Connecting to the console will give you more insight into the EMS bus traffic, MQTT queues and the actual device information. The console is reachable via Telnet (port 22) or via the Serial port if using an USB (on baud 115200). To change any settings in the console you must be admin (use `su` with the default password `ems-esp-neo`). On an ESP8266 the Serial port is disabled by default unless it's unable to connect to the WiFi. +Connecting to the console will give you more insight into the EMS bus traffic, MQTT queues and the full device information. -The `call` command is to execute a command. The command names (`[cmd]`) are the same as the MQTT command listed in the next section. - -(* = available in su/Admin mode) - -``` -common commands available in all contexts: - exit - help - log [level] - watch [ID] - su - -(from the root) - system (enters a context) - boiler (enters a context) - thermostat (enters a context) - set - fetch - scan devices [deep] * - send telegram <"XX XX ..."> * - set bus_id * - set tx_mode * - show - show devices - show ems - show values - show mqtt - -system - set - show - format * - show users * - passwd * - restart * - set wifi hostname * - set wifi password * - set wifi ssid * - wifi reconnect * - pin [data] * - -boiler - read * - call [cmd] [data] * - -thermostat - set - set master [device ID] * - read * - call [cmd] [data] [heating circuit] * - -``` +The console is reachable via Telnet (port 22) or via the Serial port if using an USB (on baud 115200). To change any settings in the console you must be admin (use `su` with the default password `ems-esp-neo`). ----------- -## **MQTT commands** +Some of the most common commands are: + * `help` lists the commands and keywords. This works in each context. + * `exit` will exit the console or exit the current context. `CTRL-D` does the same. + * `CTRL-U` for Undo + * `` for auto-complete + * Some specific commands are behind contexts. Think of this as a sub-menu. e.g. `system`, `thermostat`. The path will always show you which context you are in. `$` is the root. + * `su` will switch to the Admin super-user. The default password is `ems-esp-neo` and can be changed with `passwd` from the system menu or via the Web interface (called secret password). When in Admin mode the command prompt switches from `$` to `#`. + * Some settings can be changed in the console. The `set` command will list them. + * `show` shows the data specific to the which context you're in. From the root it will show you all the EMS device information and any external temperature sensors. + * `log` sets the logging level. `log off` disables logging. Use `log debug` for debugging commands and actions. This will be reset next time the console is opened. + * `watch` will output the incoming Rx telegrams directly to the console. You can also put on a watch on a specific EMS device ID or telegram ID. Also choose to output as verbose text as raw data bytes. -Breaking change: The MQTT base has been removed in version 2. The hostname is only used as prefixed to the topic, e.g. `ems-esp/status`. +The `call` command is to execute a command. The command names (`[cmd]`) are the same as the MQTT commands used in MQTT. -All commands must be written as `{"cmd": ,"data":, "id":}`. +For further details refer to the [Wiki](https://bbqkees-electronics.nl/wiki/). -The `id` can be replaced with `hc` for some devices and represented as a string or a number. `cmd` is a string, `data` can be a string or number. +## **Support Information** -``` -*boiler_cmd* - comfort - flowtemp - wwtemp - boilhyston (negative value) - boilhystoff (positive value) - burnperiod - burnminpower <%> - burnmaxpower <%> - pumpdelay +For a list of the EMS devices currently supported see BBQKees's [EMS device compatibility list](https://bbqkees-electronics.nl/ems-device-compatibility/). -*thermostat_cmd* ---- without hc --- - wwmode - calinttemp - minexttemp - building - language (0=de, 1=nl, 2=fr, 3=it) only RC30 - display (0=int temp, 1= int set, 2=ext. temp, 3=burner, 4=ww, 5=mode, 6=time, 7=date, 8=smoke) only RC30 - clockoffset (only RC30) ---- with hc --- - mode - temp - nighttemp - daytemp - nofrosttemp - ecotemp - heattemp - summertemp - designtemp - offsettemp - holidaytemp - remotetemp - control <0 | 1 | 2> - pause - party - holiday - date +If you're looking for support on **EMS-ESP** there are some options available: -*system_cmd* - send <"0B XX XX .."> - pin +### Documentation -``` +* [Documentation Site](https://bbqkees-electronics.nl/wiki/): For information on how to build and upload the firmware maintained by @BBQKees +* [FAQ and Troubleshooting](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html): For information on common problems and solutions + +### Support's Community + +* [EMS-ESP Support Chat](https://gitter.im/EMS-ESP/community#): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community +* [Search in Issues](https://github.com/proddy/EMS-ESP/issues): You might find an answer to your question by searching current or closed issues + +### Developers' Community + +* [Bug Report](https://github.com/proddy/EMS-ESP/issues/new?template=bug_report.md): For reporting Bugs +* [Feature Request](https://github.com/proddy/EMS-ESP/issues/new?template=feature_request.md): For requesting features/functions +* [Troubleshooting](https://github.com/proddy/EMS-ESP/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new *Troubleshooting & Question* issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer + +## **Contribute** + +You can contribute to EMS-ESP by +- providing Pull Requests (Features, Fixes, suggestions) +- testing new released features and report issues on your EMS equipment +- contributing missing [documentation](https://bbqkees-electronics.nl/wiki/) for features and devices + +## **Credits** + +A shout out to the people helping EMS-ESP get to where it is today +- @MichaelDvP for all his amazing contributions and patience. The core UART code is his. +- @BBQkees for his endless testing and building the awesome circuits +- @susisstrolch for writing a first working version of the EMS bridge circuit which I used to design EMS-ESP version 0.1 +- Plus many more providing suggestions, PRs and Donations. Thanks! + +## **License** + +This program is licensed under GPL-3.0 diff --git a/doc/MQTT.md b/doc/MQTT.md new file mode 100644 index 000000000..df9fc432c --- /dev/null +++ b/doc/MQTT.md @@ -0,0 +1,55 @@ +# **MQTT commands** + +All commands must be written as `{"cmd": ,"data":, "id":}`. + +The `id` can be replaced with `hc` for some devices that use heating circuits, and represented either as a string or a number. `cmd` is a string, `data` can be a string or number. + +topic = *boiler_cmd* +``` + comfort + flowtemp + wwtemp + boilhyston (negative value) + boilhystoff (positive value) + burnperiod + burnminpower <%> + burnmaxpower <%> + pumpdelay +``` + +topic = *thermostat_cmd* +``` +--- without hc --- + wwmode + calinttemp + minexttemp + building + language (0=de, 1=nl, 2=fr, 3=it) only RC30 + display (0=int temp, 1= int set, 2=ext. temp, 3=burner, 4=ww, 5=mode, 6=time, 7=date, 8=smoke) only RC30 + clockoffset (only RC30) + +--- with hc --- + mode + temp + nighttemp + daytemp + nofrosttemp + ecotemp + heattemp + summertemp + designtemp + offsettemp + holidaytemp + remotetemp + control <0 | 1 | 2> + pause + party + holiday + date +``` + +topic = *system_cmd* +``` + send <"0B XX XX .."> + pin +``` \ No newline at end of file diff --git a/doc/coding.md b/doc/coding.md index 891b99ab3..b0a4c13ae 100644 --- a/doc/coding.md +++ b/doc/coding.md @@ -1,6 +1,5 @@ # Notes on customizing the code - ## **Basic Design Principles** - The core services like telnet, logging and shell are based off the libraries from @nomis. I also adopted his general design pattens such as making everything as asynchronous as possible so that no one operation should starve another operation of it's time to execute (https://isocpp.org/wiki/faq/ctors#static-init-order). @@ -67,5 +66,15 @@ The Web is based off Rick's awesome [esp8266-react](https://github.com/rjwats/es * `factory_settings.ini` modified with `ems-esp-neo` as password and `ems-esp` everywhere else +## To develop and test the Web UI +- uncomment the `-D ENABLE_CORS` in `platformio.ini` +```sh +cd interface +npm start +``` +## To test the core, standalone with an ESP +```sh +make run +``` diff --git a/doc/console.md b/doc/console.md new file mode 100644 index 000000000..d4105c3d1 --- /dev/null +++ b/doc/console.md @@ -0,0 +1,72 @@ +# **Console commands** + + +Connecting to the console will give you more insight into the EMS bus traffic, MQTT queues and the full device information. + +The console is reachable via Telnet (port 22) or via the Serial port if using an USB (on baud 115200). To change any settings in the console you must be admin (use `su` with the default password `ems-esp-neo`). + +Some of the most common commands are: + * `help` lists the commands and keywords. This works in each context. + * `exit` will exit the console or exit the current context. `CTRL-D` does the same. + * `CTRL-U` for Undo + * `` for auto-complete + * Some specific commands are behind contexts. Think of this as a sub-menu. e.g. `system`, `thermostat`. The path will always show you which context you are in. `$` is the root. + * `su` will switch to the Admin super-user. The default password is `ems-esp-neo` and can be changed with `passwd` from the system menu or via the Web interface (called secret password). When in Admin mode the command prompt switches from `$` to `#`. + * Some settings can be changed in the console. The `set` command will list them. + * `show` shows the data specific to the which context you're in. From the root it will show you all the EMS device information and any external temperature sensors. + * `log` sets the logging level. `log off` disables logging. Use `log debug` for debugging commands and actions. This will be reset next time the console is opened. + * `watch` will output the incoming Rx telegrams directly to the console. You can also put on a watch on a specific EMS device ID or telegram ID. Also choose to output as verbose text as raw data bytes. + +The `call` command is to execute a command. The command names (`[cmd]`) are the same as the MQTT commands used in MQTT. + +``` +(* = available in su/Admin mode) + +common commands available in all contexts: + exit + help + log [level] + watch [ID] + su + +(from the root) + system (enters a context) + boiler (enters a context) + thermostat (enters a context) + set + fetch + scan devices [deep] * + send telegram <"XX XX ..."> * + set bus_id * + set tx_mode * + show + show devices + show ems + show values + show mqtt + read * + +system + set + show + format * + show users * + passwd * + restart * + set wifi hostname * + set wifi password * + set wifi ssid * + wifi reconnect * + pin [data] * + +boiler + read * + call [cmd] [data] * + +thermostat + set + set master [device ID] * + read * + call [cmd] [data] [heating circuit] * + +``` \ No newline at end of file diff --git a/interface/src/mqtt/MqttSettingsForm.tsx b/interface/src/mqtt/MqttSettingsForm.tsx index 153c38db5..818c3954f 100644 --- a/interface/src/mqtt/MqttSettingsForm.tsx +++ b/interface/src/mqtt/MqttSettingsForm.tsx @@ -150,15 +150,75 @@ class MqttSettingsForm extends React.Component { 2 + + + + + diff --git a/interface/src/mqtt/types.ts b/interface/src/mqtt/types.ts index 7f9bf44c8..7db88dbfd 100644 --- a/interface/src/mqtt/types.ts +++ b/interface/src/mqtt/types.ts @@ -27,7 +27,12 @@ export interface MqttSettings { keep_alive: number; clean_session: boolean; max_topic_length: number; - publish_time: number; + publish_time_boiler: number; + publish_time_thermostat: number; + publish_time_solar: number; + publish_time_mixing: number; + publish_time_other: number; + publish_time_sensor: number; mqtt_format: number; mqtt_qos: number; system_heartbeat: boolean; diff --git a/interface/src/project/EMSESPDevicesForm.tsx b/interface/src/project/EMSESPDevicesForm.tsx index fdf77a36f..8d1656e33 100644 --- a/interface/src/project/EMSESPDevicesForm.tsx +++ b/interface/src/project/EMSESPDevicesForm.tsx @@ -83,8 +83,11 @@ class EMSESPDevicesForm extends Component - - Devices: + + Devices + + + (click to show details) {!this.noDevices() && ( @@ -145,14 +148,14 @@ class EMSESPDevicesForm extends Component

- Sensors: + Sensors {!this.noSensors() && (
ID - Temperature + Temperature @@ -161,8 +164,8 @@ class EMSESPDevicesForm extends Component {sensorData.id} - - {sensorData.temp}°C + + {sensorData.temp.toFixed(1)}°C ))} @@ -285,7 +288,7 @@ class EMSESPDevicesForm extends Component {deviceData.name} - + {deviceData.value} diff --git a/interface/src/project/EMSESPHelp.tsx b/interface/src/project/EMSESPHelp.tsx index ca8778f83..afa6131f4 100644 --- a/interface/src/project/EMSESPHelp.tsx +++ b/interface/src/project/EMSESPHelp.tsx @@ -10,14 +10,14 @@ class EMSESPHelp extends Component { - EMS-ESP is an open-source firmware to communicate with heating devices that support the EMS protocol, such as equipment from Bosch, Junkers, Nefit, Buderus and Worcester. + EMS-ESP is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with EMS (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.

Please consider supporting this project via the GitHub page {'http://github.com/proddy/EMS-ESP'}.


- Check for news and updates on the {'Wiki'}. + Check for news and updates on the {'Wiki'}. For live community chat go to {'Gitter'}. diff --git a/interface/src/project/EMSESPSettingsController.tsx b/interface/src/project/EMSESPSettingsController.tsx index 157e0c235..e728a8356 100644 --- a/interface/src/project/EMSESPSettingsController.tsx +++ b/interface/src/project/EMSESPSettingsController.tsx @@ -48,9 +48,13 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps) - Customize EMS-ESP by editing the default settings here. Refer to the {'Wiki'} for descriptions of each setting. + Customize EMS-ESP by modifying the default settings here. +

+ + EMS Bus Settings + - +

+ + Dallas Sensor Settings + + + } + label="Dallas Parasite Mode" + /> +

+ + LED Settings + + } - label="Hide LED" + label="Invert/Hide LED" /> +

+ + Shower Settings + +

+ + Syslog Settings + + INFO DEBUG - {data.syslog_level !== -1 && - - - - - } + } variant="contained" color="primary" type="submit"> Save diff --git a/interface/src/project/EMSESPStatusForm.tsx b/interface/src/project/EMSESPStatusForm.tsx index a3c084fb3..b9acfcc89 100644 --- a/interface/src/project/EMSESPStatusForm.tsx +++ b/interface/src/project/EMSESPStatusForm.tsx @@ -37,6 +37,10 @@ import { import { EMSESPStatus } from "./EMSESPtypes"; +function formatNumber(num: number) { + return new Intl.NumberFormat().format(num); +} + type EMSESPStatusFormProps = RestFormProps & WithTheme & WithWidthProps; const StyledTableCell = withStyles((theme: Theme) => @@ -75,7 +79,7 @@ class EMSESPStatusForm extends Component { Statistic - # Telegrams + # Telegrams @@ -83,25 +87,25 @@ class EMSESPStatusForm extends Component { (Rx) Received telegrams - {data.rx_received} + {formatNumber(data.rx_received)} (Rx) Incomplete telegrams - {data.crc_errors} + {formatNumber(data.crc_errors)} (Tx) Successfully sent telegrams - {data.tx_sent} + {formatNumber(data.tx_sent)} (Tx) Send Errors - {data.tx_errors} + {formatNumber(data.tx_errors)}
diff --git a/interface/src/project/EMSESPtypes.ts b/interface/src/project/EMSESPtypes.ts index 9b94c7f6a..2f59596b1 100644 --- a/interface/src/project/EMSESPtypes.ts +++ b/interface/src/project/EMSESPtypes.ts @@ -7,11 +7,12 @@ export interface EMSESPSettings { master_thermostat: number; shower_timer: boolean; shower_alert: boolean; - hide_led: boolean; rx_gpio: number; - tx_gpio : number; - dallas_gpio : number; - led_gpio : number; + tx_gpio: number; + dallas_gpio: number; + dallas_parasite: boolean; + led_gpio: number; + hide_led: boolean; } export enum busConnectionStatus { diff --git a/lib/ESPAsyncWebServer/SPIFFSEditor.h b/lib/ESPAsyncWebServer/SPIFFSEditor.h index 3586429e1..aab1187a3 100644 --- a/lib/ESPAsyncWebServer/SPIFFSEditor.h +++ b/lib/ESPAsyncWebServer/SPIFFSEditor.h @@ -2,23 +2,29 @@ #define SPIFFSEditor_H_ #include -class SPIFFSEditor: public AsyncWebHandler { +class SPIFFSEditor : public AsyncWebHandler { private: - fs::FS _fs; - String _username; - String _password; - bool _authenticated; + fs::FS _fs; + String _username; + String _password; + bool _authenticated; uint32_t _startTime; + public: #ifdef ESP32 - SPIFFSEditor(const fs::FS& fs, const String& username=String(), const String& password=String()); + SPIFFSEditor(const fs::FS & fs, const String & username = String(), const String & password = String()); #else - SPIFFSEditor(const String& username=String(), const String& password=String(), const fs::FS& fs=SPIFFS); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + SPIFFSEditor(const String & username = String(), const String & password = String(), const fs::FS & fs = SPIFFS); +#pragma GCC diagnostic pop #endif - virtual bool canHandle(AsyncWebServerRequest *request) override final; - virtual void handleRequest(AsyncWebServerRequest *request) override final; - virtual void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) override final; - virtual bool isRequestHandlerTrivial() override final {return false;} + virtual bool canHandle(AsyncWebServerRequest * request) override final; + virtual void handleRequest(AsyncWebServerRequest * request) override final; + virtual void handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final) override final; + virtual bool isRequestHandlerTrivial() override final { + return false; + } }; #endif diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index 66fdd1c04..57339cde7 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -15,12 +15,12 @@ void APSettingsService::begin() { } void APSettingsService::reconfigureAP() { - _lastManaged = millis() - MANAGE_NETWORK_DELAY; + _lastManaged = uuid::get_uptime() - MANAGE_NETWORK_DELAY; _reconfigureAp = true; } void APSettingsService::loop() { - unsigned long currentMillis = millis(); + unsigned long currentMillis = uuid::get_uptime(); unsigned long manageElapsed = (unsigned long)(currentMillis - _lastManaged); if (manageElapsed >= MANAGE_NETWORK_DELAY) { _lastManaged = currentMillis; diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index 01042d9d8..991edb23d 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -7,6 +7,8 @@ #include #include +#include + #define MANAGE_NETWORK_DELAY 10000 diff --git a/lib/framework/FSPersistence.h b/lib/framework/FSPersistence.h index a7176e8dc..09be212ea 100644 --- a/lib/framework/FSPersistence.h +++ b/lib/framework/FSPersistence.h @@ -31,6 +31,12 @@ class FSPersistence { DeserializationError error = deserializeJson(jsonDocument, settingsFile); if (error == DeserializationError::Ok && jsonDocument.is()) { JsonObject jsonObject = jsonDocument.as(); + + // debug added by Proddy + // Serial.printf("Read File: %s: ", _filePath); + // serializeJson(jsonDocument, Serial); + // Serial.println(); + _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater); settingsFile.close(); return; @@ -57,9 +63,12 @@ class FSPersistence { return false; } - // Serial.printf("Write File: %s: ", _filePath); - // serializeJson(jsonDocument, Serial); - // Serial.println(); +// debug added by Proddy +#if defined(EMSESP_DEBUG) + Serial.printf("Write File: %s: ", _filePath); + serializeJson(jsonDocument, Serial); + Serial.println(); +#endif // serialize the data to the file serializeJson(jsonDocument, settingsFile); diff --git a/lib/framework/MqttSettingsService.cpp b/lib/framework/MqttSettingsService.cpp index a00baab1b..15b10340f 100644 --- a/lib/framework/MqttSettingsService.cpp +++ b/lib/framework/MqttSettingsService.cpp @@ -62,7 +62,7 @@ void MqttSettingsService::begin() { } void MqttSettingsService::loop() { - if (_reconfigureMqtt || (_disconnectedAt && (unsigned long)(millis() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) { + if (_reconfigureMqtt || (_disconnectedAt && (unsigned long)(uuid::get_uptime() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) { // reconfigure MQTT client configureMqtt(); @@ -107,7 +107,7 @@ void MqttSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reaso // Serial.print(F("Disconnected from MQTT reason: ")); // Serial.println((uint8_t)reason); _disconnectReason = reason; - _disconnectedAt = millis(); + _disconnectedAt = uuid::get_uptime(); } void MqttSettingsService::onConfigUpdated() { @@ -184,10 +184,15 @@ void MqttSettings::read(MqttSettings & settings, JsonObject & root) { root["max_topic_length"] = settings.maxTopicLength; // added by proddy for EMS-ESP - root["system_heartbeat"] = settings.system_heartbeat; - root["publish_time"] = settings.publish_time; - root["mqtt_format"] = settings.mqtt_format; - root["mqtt_qos"] = settings.mqtt_qos; + root["system_heartbeat"] = settings.system_heartbeat; + root["publish_time_boiler"] = settings.publish_time_boiler; + root["publish_time_thermostat"] = settings.publish_time_thermostat; + root["publish_time_solar"] = settings.publish_time_solar; + root["publish_time_mixing"] = settings.publish_time_mixing; + root["publish_time_other"] = settings.publish_time_other; + root["publish_time_sensor"] = settings.publish_time_sensor; + root["mqtt_format"] = settings.mqtt_format; + root["mqtt_qos"] = settings.mqtt_qos; } StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & settings) { @@ -203,10 +208,15 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting newSettings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION; newSettings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH; - newSettings.system_heartbeat = root["system_heartbeat"] | EMSESP_DEFAULT_SYSTEM_HEARTBEAT; - newSettings.publish_time = root["publish_time"] | EMSESP_DEFAULT_PUBLISH_TIME; - newSettings.mqtt_format = root["mqtt_format"] | EMSESP_DEFAULT_MQTT_FORMAT; - newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS; + newSettings.system_heartbeat = root["system_heartbeat"] | EMSESP_DEFAULT_SYSTEM_HEARTBEAT; + newSettings.publish_time_boiler = root["publish_time_boiler"] | EMSESP_DEFAULT_PUBLISH_TIME; + newSettings.publish_time_thermostat = root["publish_time_thermostat"] | EMSESP_DEFAULT_PUBLISH_TIME; + newSettings.publish_time_solar = root["publish_time_solar"] | EMSESP_DEFAULT_PUBLISH_TIME; + newSettings.publish_time_mixing = root["publish_time_mixing"] | EMSESP_DEFAULT_PUBLISH_TIME; + newSettings.publish_time_other = root["publish_time_other"] | EMSESP_DEFAULT_PUBLISH_TIME; + newSettings.publish_time_sensor = root["publish_time_sensor"] | EMSESP_DEFAULT_PUBLISH_TIME; + newSettings.mqtt_format = root["mqtt_format"] | EMSESP_DEFAULT_MQTT_FORMAT; + newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS; if (newSettings.system_heartbeat != settings.system_heartbeat) { emsesp::EMSESP::system_.set_heartbeat(newSettings.system_heartbeat); @@ -216,8 +226,23 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting emsesp::EMSESP::mqtt_.set_qos(newSettings.mqtt_qos); } - if (newSettings.publish_time != settings.publish_time) { - emsesp::EMSESP::mqtt_.set_publish_time(newSettings.publish_time); + if (newSettings.publish_time_boiler != settings.publish_time_boiler) { + emsesp::EMSESP::mqtt_.set_publish_time_boiler(newSettings.publish_time_boiler); + } + if (newSettings.publish_time_thermostat != settings.publish_time_thermostat) { + emsesp::EMSESP::mqtt_.set_publish_time_thermostat(newSettings.publish_time_thermostat); + } + if (newSettings.publish_time_solar != settings.publish_time_solar) { + emsesp::EMSESP::mqtt_.set_publish_time_solar(newSettings.publish_time_solar); + } + if (newSettings.publish_time_mixing != settings.publish_time_mixing) { + emsesp::EMSESP::mqtt_.set_publish_time_mixing(newSettings.publish_time_mixing); + } + if (newSettings.publish_time_other != settings.publish_time_other) { + emsesp::EMSESP::mqtt_.set_publish_time_other(newSettings.publish_time_other); + } + if (newSettings.publish_time_sensor != settings.publish_time_sensor) { + emsesp::EMSESP::mqtt_.set_publish_time_sensor(newSettings.publish_time_sensor); } emsesp::EMSESP::mqtt_.reset_publish_fails(); // reset fail counter back to 0 diff --git a/lib/framework/MqttSettingsService.h b/lib/framework/MqttSettingsService.h index 42bc5ab55..573a13b6c 100644 --- a/lib/framework/MqttSettingsService.h +++ b/lib/framework/MqttSettingsService.h @@ -6,12 +6,13 @@ #include #include #include +#include #include "../../src/system.h" #include "../../src/mqtt.h" #include "../../src/sensors.h" -#define MQTT_RECONNECTION_DELAY 5000 +#define MQTT_RECONNECTION_DELAY 1000 #define MQTT_SETTINGS_FILE "/config/mqttSettings.json" #define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings" @@ -85,7 +86,12 @@ class MqttSettings { uint16_t maxTopicLength; // proddy EMS-ESP specific - uint16_t publish_time; // seconds + uint16_t publish_time_boiler; + uint16_t publish_time_thermostat; + uint16_t publish_time_solar; + uint16_t publish_time_mixing; + uint16_t publish_time_other; + uint16_t publish_time_sensor; uint8_t mqtt_format; // 1=single, 2=nested, 3=ha, 4=custom uint8_t mqtt_qos; bool system_heartbeat; diff --git a/lib/framework/NTPStatus.cpp b/lib/framework/NTPStatus.cpp index 2275d2f76..5afb93b23 100644 --- a/lib/framework/NTPStatus.cpp +++ b/lib/framework/NTPStatus.cpp @@ -33,7 +33,7 @@ void NTPStatus::ntpStatus(AsyncWebServerRequest* request) { root["server"] = sntp_getservername(0); // device uptime in seconds - root["uptime"] = millis() / 1000; + root["uptime"] = uuid::get_uptime() / 1000; response->setLength(); request->send(response); diff --git a/lib/framework/NTPStatus.h b/lib/framework/NTPStatus.h index 7bb918059..893435847 100644 --- a/lib/framework/NTPStatus.h +++ b/lib/framework/NTPStatus.h @@ -16,6 +16,8 @@ #include #include #include +#include + #define MAX_NTP_STATUS_SIZE 1024 #define NTP_STATUS_SERVICE_PATH "/rest/ntpStatus" diff --git a/lib/framework/SystemStatus.h b/lib/framework/SystemStatus.h index ab3f33b58..1ea61ae1e 100644 --- a/lib/framework/SystemStatus.h +++ b/lib/framework/SystemStatus.h @@ -8,7 +8,7 @@ #elif defined(ESP8266) #include #include -#include +// #include #include // proddy added #endif diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index c6b08c3bf..d19b29424 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -24,12 +24,18 @@ class DummySettings { bool shower_alert = false; bool hide_led = false; uint16_t publish_time = 10; // seconds - uint8_t mqtt_format = 1; // 1=single, 2=nested, 3=ha, 4=custom + uint8_t mqtt_format = 3; // 1=single, 2=nested, 3=ha, 4=custom uint8_t mqtt_qos = 0; String hostname = "ems-esp"; String jwtSecret = "ems-esp"; String ssid = "ems-esp"; String password = "ems-esp"; + uint16_t publish_time_boiler; + uint16_t publish_time_thermostat; + uint16_t publish_time_solar; + uint16_t publish_time_mixing; + uint16_t publish_time_other; + uint16_t publish_time_sensor; static void read(DummySettings & settings, JsonObject & root){}; static void read(DummySettings & settings){}; diff --git a/makefile b/makefile index 06928a441..62892b682 100644 --- a/makefile +++ b/makefile @@ -26,7 +26,7 @@ CXX_STANDARD := -std=c++11 #---------------------------------------------------------------------- # Defined Symbols #---------------------------------------------------------------------- -DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_NO_LED +DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DEMSESP_DEBUG -DEMSESP_STANDALONE #---------------------------------------------------------------------- # Sources & Files diff --git a/media/console.PNG b/media/console.PNG new file mode 100644 index 000000000..de453e40b Binary files /dev/null and b/media/console.PNG differ diff --git a/media/gateway-integration.jpg b/media/gateway-integration.jpg new file mode 100644 index 000000000..c5ca938f4 Binary files /dev/null and b/media/gateway-integration.jpg differ diff --git a/platformio.ini b/platformio.ini index dcb5e055e..10961b3d5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,11 +10,9 @@ extra_configs = pio_local.ini [common] -;debug_flags = -DDEBUG_ESP_PORT=Serial -DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM debug_flags = ; -D EMSESP_DEBUG ; -D EMSESP_FORCE_SERIAL - ; -D EMSESP_NO_LED ; default platformio compile flags are: -fno-rtti -std=c++11 -Os -mlongcalls -mtext-section-literals -falign-functions=4 -ffunction-sections -fdata-sections -fno-exceptions -Wall build_flags = @@ -85,8 +83,8 @@ lib_ignore = [env:esp32] board = esp32dev build_type = release -; platform = espressif32 -platform = https://github.com/platformio/platform-espressif32.git +platform = espressif32 +; platform = https://github.com/platformio/platform-espressif32.git board_build.partitions = min_spiffs.csv ; https://github.com/espressif/arduino-esp32/blob/master/tools/partitions/ lib_deps = ${common.libs_core} build_flags = ${common.build_flags} ${common.debug_flags} diff --git a/scripts/analyze_stackdmp.py b/scripts/analyze_stackdmp.py old mode 100644 new mode 100755 diff --git a/scripts/boot_app0.bin b/scripts/boot_app0.bin new file mode 100644 index 000000000..13562cabb Binary files /dev/null and b/scripts/boot_app0.bin differ diff --git a/scripts/bootloader_dio_40m.bin b/scripts/bootloader_dio_40m.bin new file mode 100644 index 000000000..ac057886d Binary files /dev/null and b/scripts/bootloader_dio_40m.bin differ diff --git a/scripts/build_interface.py b/scripts/build_interface.py old mode 100644 new mode 100755 diff --git a/scripts/clean_fw.py b/scripts/clean_fw.py old mode 100644 new mode 100755 diff --git a/scripts/decoder.py b/scripts/decoder.py old mode 100644 new mode 100755 diff --git a/scripts/decoder_linux.py b/scripts/decoder_linux.py old mode 100644 new mode 100755 diff --git a/scripts/espota.py b/scripts/espota.py old mode 100644 new mode 100755 diff --git a/scripts/esptool.py b/scripts/esptool.py new file mode 100755 index 000000000..69df228da --- /dev/null +++ b/scripts/esptool.py @@ -0,0 +1,2959 @@ +#!/usr/bin/env python +# +# ESP8266 & ESP32 ROM Bootloader Utility +# Copyright (C) 2014-2016 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) PTE LTD, other contributors as noted. +# https://github.com/espressif/esptool +# +# This program 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 2 of the License, or (at your option) any later version. +# +# This program 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 +# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +# Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from __future__ import division, print_function + +import argparse +import base64 +import binascii +import copy +import hashlib +import inspect +import io +import os +import shlex +import struct +import sys +import time +import zlib +import string + +try: + import serial +except ImportError: + print("Pyserial is not installed for %s. Check the README for installation instructions." % (sys.executable)) + raise + +# check 'serial' is 'pyserial' and not 'serial' https://github.com/espressif/esptool/issues/269 +try: + if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__: + raise ImportError(""" +esptool.py depends on pyserial, but there is a conflict with a currently installed package named 'serial'. + +You may be able to work around this by 'pip uninstall serial; pip install pyserial' \ +but this may break other installed Python software that depends on 'serial'. + +There is no good fix for this right now, apart from configuring virtualenvs. \ +See https://github.com/espressif/esptool/issues/269#issuecomment-385298196 for discussion of the underlying issue(s).""") +except TypeError: + pass # __doc__ returns None for pyserial + +try: + import serial.tools.list_ports as list_ports +except ImportError: + print("The installed version (%s) of pyserial appears to be too old for esptool.py (Python interpreter %s). " + "Check the README for installation instructions." % (sys.VERSION, sys.executable)) + raise + +__version__ = "2.6" + +MAX_UINT32 = 0xffffffff +MAX_UINT24 = 0xffffff + +DEFAULT_TIMEOUT = 3 # timeout for most flash operations +START_FLASH_TIMEOUT = 20 # timeout for starting flash (may perform erase) +CHIP_ERASE_TIMEOUT = 120 # timeout for full chip erase +MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2 # longest any command can run +SYNC_TIMEOUT = 0.1 # timeout for syncing with bootloader +MD5_TIMEOUT_PER_MB = 8 # timeout (per megabyte) for calculating md5sum +ERASE_REGION_TIMEOUT_PER_MB = 30 # timeout (per megabyte) for erasing a region +MEM_END_ROM_TIMEOUT = 0.05 # special short timeout for ESP_MEM_END, as it may never respond +DEFAULT_SERIAL_WRITE_TIMEOUT = 10 # timeout for serial port write + + +def timeout_per_mb(seconds_per_mb, size_bytes): + """ Scales timeouts which are size-specific """ + result = seconds_per_mb * (size_bytes / 1e6) + if result < DEFAULT_TIMEOUT: + return DEFAULT_TIMEOUT + return result + + +DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB', + 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', 0x18: '16MB'} + + +def check_supported_function(func, check_func): + """ + Decorator implementation that wraps a check around an ESPLoader + bootloader function to check if it's supported. + + This is used to capture the multidimensional differences in + functionality between the ESP8266 & ESP32 ROM loaders, and the + software stub that runs on both. Not possible to do this cleanly + via inheritance alone. + """ + def inner(*args, **kwargs): + obj = args[0] + if check_func(obj): + return func(*args, **kwargs) + else: + raise NotImplementedInROMError(obj, func) + return inner + + +def stub_function_only(func): + """ Attribute for a function only supported in the software stub loader """ + return check_supported_function(func, lambda o: o.IS_STUB) + + +def stub_and_esp32_function_only(func): + """ Attribute for a function only supported by software stubs or ESP32 ROM """ + return check_supported_function(func, lambda o: o.IS_STUB or o.CHIP_NAME == "ESP32") + + +PYTHON2 = sys.version_info[0] < 3 # True if on pre-Python 3 + +# Function to return nth byte of a bitstring +# Different behaviour on Python 2 vs 3 +if PYTHON2: + def byte(bitstr, index): + return ord(bitstr[index]) +else: + def byte(bitstr, index): + return bitstr[index] + +# Provide a 'basestring' class on Python 3 +try: + basestring +except NameError: + basestring = str + + +def esp8266_function_only(func): + """ Attribute for a function only supported on ESP8266 """ + return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266") + + +class ESPLoader(object): + """ Base class providing access to ESP ROM & software stub bootloaders. + Subclasses provide ESP8266 & ESP32 specific functionality. + + Don't instantiate this base class directly, either instantiate a subclass or + call ESPLoader.detect_chip() which will interrogate the chip and return the + appropriate subclass instance. + + """ + CHIP_NAME = "Espressif device" + IS_STUB = False + + DEFAULT_PORT = "/dev/ttyUSB0" + + # Commands supported by ESP8266 ROM bootloader + ESP_FLASH_BEGIN = 0x02 + ESP_FLASH_DATA = 0x03 + ESP_FLASH_END = 0x04 + ESP_MEM_BEGIN = 0x05 + ESP_MEM_END = 0x06 + ESP_MEM_DATA = 0x07 + ESP_SYNC = 0x08 + ESP_WRITE_REG = 0x09 + ESP_READ_REG = 0x0a + + # Some comands supported by ESP32 ROM bootloader (or -8266 w/ stub) + ESP_SPI_SET_PARAMS = 0x0B + ESP_SPI_ATTACH = 0x0D + ESP_CHANGE_BAUDRATE = 0x0F + ESP_FLASH_DEFL_BEGIN = 0x10 + ESP_FLASH_DEFL_DATA = 0x11 + ESP_FLASH_DEFL_END = 0x12 + ESP_SPI_FLASH_MD5 = 0x13 + + # Some commands supported by stub only + ESP_ERASE_FLASH = 0xD0 + ESP_ERASE_REGION = 0xD1 + ESP_READ_FLASH = 0xD2 + ESP_RUN_USER_CODE = 0xD3 + + # Maximum block sized for RAM and Flash writes, respectively. + ESP_RAM_BLOCK = 0x1800 + + FLASH_WRITE_SIZE = 0x400 + + # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want. + ESP_ROM_BAUD = 115200 + + # First byte of the application image + ESP_IMAGE_MAGIC = 0xe9 + + # Initial state for the checksum routine + ESP_CHECKSUM_MAGIC = 0xef + + # Flash sector size, minimum unit of erase. + FLASH_SECTOR_SIZE = 0x1000 + + UART_DATA_REG_ADDR = 0x60000078 + + # Memory addresses + IROM_MAP_START = 0x40200000 + IROM_MAP_END = 0x40300000 + + # The number of bytes in the UART response that signify command status + STATUS_BYTES_LENGTH = 2 + + def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): + """Base constructor for ESPLoader bootloader interaction + + Don't call this constructor, either instantiate ESP8266ROM + or ESP32ROM, or use ESPLoader.detect_chip(). + + This base class has all of the instance methods for bootloader + functionality supported across various chips & stub + loaders. Subclasses replace the functions they don't support + with ones which throw NotImplementedInROMError(). + + """ + if isinstance(port, basestring): + self._port = serial.serial_for_url(port) + else: + self._port = port + self._slip_reader = slip_reader(self._port, self.trace) + # setting baud rate in a separate step is a workaround for + # CH341 driver on some Linux versions (this opens at 9600 then + # sets), shouldn't matter for other platforms/drivers. See + # https://github.com/espressif/esptool/issues/44#issuecomment-107094446 + self._set_port_baudrate(baud) + self._trace_enabled = trace_enabled + # set write timeout, to prevent esptool blocked at write forever. + try: + self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT + except NotImplementedError: + # no write timeout for RFC2217 ports + # need to set the property back to None or it will continue to fail + self._port.write_timeout = None + + def _set_port_baudrate(self, baud): + try: + self._port.baudrate = baud + except IOError: + raise FatalError("Failed to set baud rate %d. The driver may not support this rate." % baud) + + @staticmethod + def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False): + """ Use serial access to detect the chip type. + + We use the UART's datecode register for this, it's mapped at + the same address on ESP8266 & ESP32 so we can use one + memory read and compare to the datecode register for each chip + type. + + This routine automatically performs ESPLoader.connect() (passing + connect_mode parameter) as part of querying the chip. + """ + detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) + detect_port.connect(connect_mode) + try: + print('Detecting chip type...', end='') + sys.stdout.flush() + date_reg = detect_port.read_reg(ESPLoader.UART_DATA_REG_ADDR) + + for cls in [ESP8266ROM, ESP32ROM]: + if date_reg == cls.DATE_REG_VALUE: + # don't connect a second time + inst = cls(detect_port._port, baud, trace_enabled=trace_enabled) + print(' %s' % inst.CHIP_NAME, end='') + return inst + finally: + print('') # end line + raise FatalError("Unexpected UART datecode value 0x%08x. Failed to autodetect chip type." % date_reg) + + """ Read a SLIP packet from the serial port """ + def read(self): + return next(self._slip_reader) + + """ Write bytes to the serial port while performing SLIP escaping """ + def write(self, packet): + buf = b'\xc0' \ + + (packet.replace(b'\xdb',b'\xdb\xdd').replace(b'\xc0',b'\xdb\xdc')) \ + + b'\xc0' + self.trace("Write %d bytes: %s", len(buf), HexFormatter(buf)) + self._port.write(buf) + + def trace(self, message, *format_args): + if self._trace_enabled: + now = time.time() + try: + + delta = now - self._last_trace + except AttributeError: + delta = 0.0 + self._last_trace = now + prefix = "TRACE +%.3f " % delta + print(prefix + (message % format_args)) + + """ Calculate checksum of a blob, as it is defined by the ROM """ + @staticmethod + def checksum(data, state=ESP_CHECKSUM_MAGIC): + for b in data: + if type(b) is int: # python 2/3 compat + state ^= b + else: + state ^= ord(b) + + return state + + """ Send a request and read the response """ + def command(self, op=None, data=b"", chk=0, wait_response=True, timeout=DEFAULT_TIMEOUT): + saved_timeout = self._port.timeout + new_timeout = min(timeout, MAX_TIMEOUT) + if new_timeout != saved_timeout: + self._port.timeout = new_timeout + + try: + if op is not None: + self.trace("command op=0x%02x data len=%s wait_response=%d timeout=%.3f data=%s", + op, len(data), 1 if wait_response else 0, timeout, HexFormatter(data)) + pkt = struct.pack(b' self.STATUS_BYTES_LENGTH: + return data[:-self.STATUS_BYTES_LENGTH] + else: # otherwise, just return the 'val' field which comes from the reply header (this is used by read_reg) + return val + + def flush_input(self): + self._port.flushInput() + self._slip_reader = slip_reader(self._port, self.trace) + + def sync(self): + self.command(self.ESP_SYNC, b'\x07\x07\x12\x20' + 32 * b'\x55', + timeout=SYNC_TIMEOUT) + for i in range(7): + self.command() + + def _setDTR(self, state): + self._port.setDTR(state) + + def _setRTS(self, state): + self._port.setRTS(state) + # Work-around for adapters on Windows using the usbser.sys driver: + # generate a dummy change to DTR so that the set-control-line-state + # request is sent with the updated RTS state and the same DTR state + self._port.setDTR(self._port.dtr) + + def _connect_attempt(self, mode='default_reset', esp32r0_delay=False): + """ A single connection attempt, with esp32r0 workaround options """ + # esp32r0_delay is a workaround for bugs with the most common auto reset + # circuit and Windows, if the EN pin on the dev board does not have + # enough capacitance. + # + # Newer dev boards shouldn't have this problem (higher value capacitor + # on the EN pin), and ESP32 revision 1 can't use this workaround as it + # relies on a silicon bug. + # + # Details: https://github.com/espressif/esptool/issues/136 + last_error = None + + # If we're doing no_sync, we're likely communicating as a pass through + # with an intermediate device to the ESP32 + if mode == "no_reset_no_sync": + return last_error + + # issue reset-to-bootloader: + # RTS = either CH_PD/EN or nRESET (both active low = chip in reset + # DTR = GPIO0 (active low = boot to flasher) + # + # DTR & RTS are active low signals, + # ie True = pin @ 0V, False = pin @ VCC. + if mode != 'no_reset': + self._setDTR(False) # IO0=HIGH + self._setRTS(True) # EN=LOW, chip in reset + time.sleep(0.1) + if esp32r0_delay: + # Some chips are more likely to trigger the esp32r0 + # watchdog reset silicon bug if they're held with EN=LOW + # for a longer period + time.sleep(1.2) + self._setDTR(True) # IO0=LOW + self._setRTS(False) # EN=HIGH, chip out of reset + if esp32r0_delay: + # Sleep longer after reset. + # This workaround only works on revision 0 ESP32 chips, + # it exploits a silicon bug spurious watchdog reset. + time.sleep(0.4) # allow watchdog reset to occur + time.sleep(0.05) + self._setDTR(False) # IO0=HIGH, done + + for _ in range(5): + try: + self.flush_input() + self._port.flushOutput() + self.sync() + return None + except FatalError as e: + if esp32r0_delay: + print('_', end='') + else: + print('.', end='') + sys.stdout.flush() + time.sleep(0.05) + last_error = e + return last_error + + def connect(self, mode='default_reset'): + """ Try connecting repeatedly until successful, or giving up """ + print('Connecting...', end='') + sys.stdout.flush() + last_error = None + + try: + for _ in range(7): + last_error = self._connect_attempt(mode=mode, esp32r0_delay=False) + if last_error is None: + return + last_error = self._connect_attempt(mode=mode, esp32r0_delay=True) + if last_error is None: + return + finally: + print('') # end 'Connecting...' line + raise FatalError('Failed to connect to %s: %s' % (self.CHIP_NAME, last_error)) + + """ Read memory address in target """ + def read_reg(self, addr): + # we don't call check_command here because read_reg() function is called + # when detecting chip type, and the way we check for success (STATUS_BYTES_LENGTH) is different + # for different chip types (!) + val, data = self.command(self.ESP_READ_REG, struct.pack(' start: + raise FatalError(("Software loader is resident at 0x%08x-0x%08x. " + + "Can't load binary at overlapping address range 0x%08x-0x%08x. " + + "Either change binary loading address, or use the --no-stub " + + "option to disable the software loader.") % (start, end, load_start, load_end)) + + return self.check_command("enter RAM download mode", self.ESP_MEM_BEGIN, + struct.pack(' length: + raise FatalError('Read more than expected') + digest_frame = self.read() + if len(digest_frame) != 16: + raise FatalError('Expected digest, got: %s' % hexify(digest_frame)) + expected_digest = hexify(digest_frame).upper() + digest = hashlib.md5(data).hexdigest().upper() + if digest != expected_digest: + raise FatalError('Digest mismatch: expected %s, got %s' % (expected_digest, digest)) + return data + + def flash_spi_attach(self, hspi_arg): + """Send SPI attach command to enable the SPI flash pins + + ESP8266 ROM does this when you send flash_begin, ESP32 ROM + has it as a SPI command. + """ + # last 3 bytes in ESP_SPI_ATTACH argument are reserved values + arg = struct.pack(' 0: + self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1) + if miso_bits > 0: + self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1) + else: + + def set_data_lengths(mosi_bits, miso_bits): + SPI_DATA_LEN_REG = SPI_USR1_REG + SPI_MOSI_BITLEN_S = 17 + SPI_MISO_BITLEN_S = 8 + mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1) + miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1) + self.write_reg(SPI_DATA_LEN_REG, + (miso_mask << SPI_MISO_BITLEN_S) | ( + mosi_mask << SPI_MOSI_BITLEN_S)) + + # SPI peripheral "command" bitmasks for SPI_CMD_REG + SPI_CMD_USR = (1 << 18) + + # shift values + SPI_USR2_DLEN_SHIFT = 28 + + if read_bits > 32: + raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") + if len(data) > 64: + raise FatalError("Writing more than 64 bytes of data with one SPI command is unsupported") + + data_bits = len(data) * 8 + old_spi_usr = self.read_reg(SPI_USR_REG) + old_spi_usr2 = self.read_reg(SPI_USR2_REG) + flags = SPI_USR_COMMAND + if read_bits > 0: + flags |= SPI_USR_MISO + if data_bits > 0: + flags |= SPI_USR_MOSI + set_data_lengths(data_bits, read_bits) + self.write_reg(SPI_USR_REG, flags) + self.write_reg(SPI_USR2_REG, + (7 << SPI_USR2_DLEN_SHIFT) | spiflash_command) + if data_bits == 0: + self.write_reg(SPI_W0_REG, 0) # clear data register before we read it + else: + data = pad_to(data, 4, b'\00') # pad to 32-bit multiple + words = struct.unpack("I" * (len(data) // 4), data) + next_reg = SPI_W0_REG + for word in words: + self.write_reg(next_reg, word) + next_reg += 4 + self.write_reg(SPI_CMD_REG, SPI_CMD_USR) + + def wait_done(): + for _ in range(10): + if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0: + return + raise FatalError("SPI command did not complete in time") + wait_done() + + status = self.read_reg(SPI_W0_REG) + # restore some SPI controller registers + self.write_reg(SPI_USR_REG, old_spi_usr) + self.write_reg(SPI_USR2_REG, old_spi_usr2) + return status + + def read_status(self, num_bytes=2): + """Read up to 24 bits (num_bytes) of SPI flash status register contents + via RDSR, RDSR2, RDSR3 commands + + Not all SPI flash supports all three commands. The upper 1 or 2 + bytes may be 0xFF. + """ + SPIFLASH_RDSR = 0x05 + SPIFLASH_RDSR2 = 0x35 + SPIFLASH_RDSR3 = 0x15 + + status = 0 + shift = 0 + for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]: + status += self.run_spiflash_command(cmd, read_bits=8) << shift + shift += 8 + return status + + def write_status(self, new_status, num_bytes=2, set_non_volatile=False): + """Write up to 24 bits (num_bytes) of new status register + + num_bytes can be 1, 2 or 3. + + Not all flash supports the additional commands to write the + second and third byte of the status register. When writing 2 + bytes, esptool also sends a 16-byte WRSR command (as some + flash types use this instead of WRSR2.) + + If the set_non_volatile flag is set, non-volatile bits will + be set as well as volatile ones (WREN used instead of WEVSR). + + """ + SPIFLASH_WRSR = 0x01 + SPIFLASH_WRSR2 = 0x31 + SPIFLASH_WRSR3 = 0x11 + SPIFLASH_WEVSR = 0x50 + SPIFLASH_WREN = 0x06 + SPIFLASH_WRDI = 0x04 + + enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR + + # try using a 16-bit WRSR (not supported by all chips) + # this may be redundant, but shouldn't hurt + if num_bytes == 2: + self.run_spiflash_command(enable_cmd) + self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8 + + self.run_spiflash_command(SPIFLASH_WRDI) + + def hard_reset(self): + self._setRTS(True) # EN->LOW + time.sleep(0.1) + self._setRTS(False) + + def soft_reset(self, stay_in_bootloader): + if not self.IS_STUB: + if stay_in_bootloader: + return # ROM bootloader is already in bootloader! + else: + # 'run user code' is as close to a soft reset as we can do + self.flash_begin(0, 0) + self.flash_finish(False) + else: + if stay_in_bootloader: + # soft resetting from the stub loader + # will re-load the ROM bootloader + self.flash_begin(0, 0) + self.flash_finish(True) + elif self.CHIP_NAME != "ESP8266": + raise FatalError("Soft resetting is currently only supported on ESP8266") + else: + # running user code from stub loader requires some hacks + # in the stub loader + self.command(self.ESP_RUN_USER_CODE, wait_response=False) + + +class ESP8266ROM(ESPLoader): + """ Access class for ESP8266 ROM bootloader + """ + CHIP_NAME = "ESP8266" + IS_STUB = False + + DATE_REG_VALUE = 0x00062000 + + # OTP ROM addresses + ESP_OTP_MAC0 = 0x3ff00050 + ESP_OTP_MAC1 = 0x3ff00054 + ESP_OTP_MAC3 = 0x3ff0005c + + SPI_REG_BASE = 0x60000200 + SPI_W0_OFFS = 0x40 + SPI_HAS_MOSI_DLEN_REG = False + + FLASH_SIZES = { + '512KB':0x00, + '256KB':0x10, + '1MB':0x20, + '2MB':0x30, + '4MB':0x40, + '2MB-c1': 0x50, + '4MB-c1':0x60, + '8MB':0x80, + '16MB':0x90, + } + + BOOTLOADER_FLASH_OFFSET = 0 + + def get_efuses(self): + # Return the 128 bits of ESP8266 efuse as a single Python integer + return (self.read_reg(0x3ff0005c) << 96 | + self.read_reg(0x3ff00058) << 64 | + self.read_reg(0x3ff00054) << 32 | + self.read_reg(0x3ff00050)) + + def get_chip_description(self): + efuses = self.get_efuses() + is_8285 = (efuses & ((1 << 4) | 1 << 80)) != 0 # One or the other efuse bit is set for ESP8285 + return "ESP8285" if is_8285 else "ESP8266EX" + + def get_chip_features(self): + features = ["WiFi"] + if self.get_chip_description() == "ESP8285": + features += ["Embedded Flash"] + return features + + def flash_spi_attach(self, hspi_arg): + if self.IS_STUB: + super(ESP8266ROM, self).flash_spi_attach(hspi_arg) + else: + # ESP8266 ROM has no flash_spi_attach command in serial protocol, + # but flash_begin will do it + self.flash_begin(0, 0) + + def flash_set_parameters(self, size): + # not implemented in ROM, but OK to silently skip for ROM + if self.IS_STUB: + super(ESP8266ROM, self).flash_set_parameters(size) + + def chip_id(self): + """ Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() function """ + id0 = self.read_reg(self.ESP_OTP_MAC0) + id1 = self.read_reg(self.ESP_OTP_MAC1) + return (id0 >> 24) | ((id1 & MAX_UINT24) << 8) + + def read_mac(self): + """ Read MAC from OTP ROM """ + mac0 = self.read_reg(self.ESP_OTP_MAC0) + mac1 = self.read_reg(self.ESP_OTP_MAC1) + mac3 = self.read_reg(self.ESP_OTP_MAC3) + if (mac3 != 0): + oui = ((mac3 >> 16) & 0xff, (mac3 >> 8) & 0xff, mac3 & 0xff) + elif ((mac1 >> 16) & 0xff) == 0: + oui = (0x18, 0xfe, 0x34) + elif ((mac1 >> 16) & 0xff) == 1: + oui = (0xac, 0xd0, 0x74) + else: + raise FatalError("Unknown OUI") + return oui + ((mac1 >> 8) & 0xff, mac1 & 0xff, (mac0 >> 24) & 0xff) + + def get_erase_size(self, offset, size): + """ Calculate an erase size given a specific size in bytes. + + Provides a workaround for the bootloader erase bug.""" + + sectors_per_block = 16 + sector_size = self.FLASH_SECTOR_SIZE + num_sectors = (size + sector_size - 1) // sector_size + start_sector = offset // sector_size + + head_sectors = sectors_per_block - (start_sector % sectors_per_block) + if num_sectors < head_sectors: + head_sectors = num_sectors + + if num_sectors < 2 * head_sectors: + return (num_sectors + 1) // 2 * sector_size + else: + return (num_sectors - head_sectors) * sector_size + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("Overriding VDDSDIO setting only applies to ESP32") + + +class ESP8266StubLoader(ESP8266ROM): + """ Access class for ESP8266 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + IS_STUB = True + + def __init__(self, rom_loader): + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + def get_erase_size(self, offset, size): + return size # stub doesn't have same size bug as ROM loader + + +ESP8266ROM.STUB_CLASS = ESP8266StubLoader + + +class ESP32ROM(ESPLoader): + """Access class for ESP32 ROM bootloader + + """ + CHIP_NAME = "ESP32" + IS_STUB = False + + DATE_REG_VALUE = 0x15122500 + + IROM_MAP_START = 0x400d0000 + IROM_MAP_END = 0x40400000 + DROM_MAP_START = 0x3F400000 + DROM_MAP_END = 0x3F800000 + + # ESP32 uses a 4 byte status reply + STATUS_BYTES_LENGTH = 4 + + SPI_REG_BASE = 0x60002000 + EFUSE_REG_BASE = 0x6001a000 + + SPI_W0_OFFS = 0x80 + SPI_HAS_MOSI_DLEN_REG = True + + FLASH_SIZES = { + '1MB':0x00, + '2MB':0x10, + '4MB':0x20, + '8MB':0x30, + '16MB':0x40 + } + + BOOTLOADER_FLASH_OFFSET = 0x1000 + + OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"] + + def get_chip_description(self): + word3 = self.read_efuse(3) + chip_ver_rev1 = (word3 >> 15) & 0x1 + pkg_version = (word3 >> 9) & 0x07 + + chip_name = { + 0: "ESP32D0WDQ6", + 1: "ESP32D0WDQ5", + 2: "ESP32D2WDQ5", + 5: "ESP32-PICO-D4", + }.get(pkg_version, "unknown ESP32") + + return "%s (revision %d)" % (chip_name, chip_ver_rev1) + + def get_chip_features(self): + features = ["WiFi"] + word3 = self.read_efuse(3) + + # names of variables in this section are lowercase + # versions of EFUSE names as documented in TRM and + # ESP-IDF efuse_reg.h + + chip_ver_dis_bt = word3 & (1 << 1) + if chip_ver_dis_bt == 0: + features += ["BT"] + + chip_ver_dis_app_cpu = word3 & (1 << 0) + if chip_ver_dis_app_cpu: + features += ["Single Core"] + else: + features += ["Dual Core"] + + chip_cpu_freq_rated = word3 & (1 << 13) + if chip_cpu_freq_rated: + chip_cpu_freq_low = word3 & (1 << 12) + if chip_cpu_freq_low: + features += ["160MHz"] + else: + features += ["240MHz"] + + pkg_version = (word3 >> 9) & 0x07 + if pkg_version in [2, 4, 5]: + features += ["Embedded Flash"] + + word4 = self.read_efuse(4) + adc_vref = (word4 >> 8) & 0x1F + if adc_vref: + features += ["VRef calibration in efuse"] + + blk3_part_res = word3 >> 14 & 0x1 + if blk3_part_res: + features += ["BLK3 partially reserved"] + + word6 = self.read_efuse(6) + coding_scheme = word6 & 0x3 + features += ["Coding Scheme %s" % { + 0: "None", + 1: "3/4", + 2: "Repeat (UNSUPPORTED)", + 3: "Invalid"}[coding_scheme]] + + return features + + def read_efuse(self, n): + """ Read the nth word of the ESP3x EFUSE region. """ + return self.read_reg(self.EFUSE_REG_BASE + (4 * n)) + + def chip_id(self): + raise NotSupportedError(self, "chip_id") + + def read_mac(self): + """ Read MAC from EFUSE region """ + words = [self.read_efuse(2), self.read_efuse(1)] + bitstring = struct.pack(">II", *words) + bitstring = bitstring[2:8] # trim the 2 byte CRC + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_erase_size(self, offset, size): + return size + + def override_vddsdio(self, new_voltage): + new_voltage = new_voltage.upper() + if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES: + raise FatalError("The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'") + RTC_CNTL_SDIO_CONF_REG = 0x3ff48074 + RTC_CNTL_XPD_SDIO_REG = (1 << 31) + RTC_CNTL_DREFH_SDIO_M = (3 << 29) + RTC_CNTL_DREFM_SDIO_M = (3 << 27) + RTC_CNTL_DREFL_SDIO_M = (3 << 25) + # RTC_CNTL_SDIO_TIEH = (1 << 23) # not used here, setting TIEH=1 would set 3.3V output, not safe for esptool.py to do + RTC_CNTL_SDIO_FORCE = (1 << 22) + RTC_CNTL_SDIO_PD_EN = (1 << 21) + + reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting + reg_val |= RTC_CNTL_SDIO_PD_EN + if new_voltage != "OFF": + reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO + if new_voltage == "1.9V": + reg_val |= (RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M) # boost voltage + self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val) + print("VDDSDIO regulator set to %s" % new_voltage) + + +class ESP32StubLoader(ESP32ROM): + """ Access class for ESP32 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32ROM.STUB_CLASS = ESP32StubLoader + + +class ESPBOOTLOADER(object): + """ These are constants related to software ESP bootloader, working with 'v2' image files """ + + # First byte of the "v2" application image + IMAGE_V2_MAGIC = 0xea + + # First 'segment' value in a "v2" application image, appears to be a constant version value? + IMAGE_V2_SEGMENT = 4 + + +def LoadFirmwareImage(chip, filename): + """ Load a firmware image. Can be for ESP8266 or ESP32. ESP8266 images will be examined to determine if they are + original ROM firmware images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images. + + Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) or ESP8266V2FirmwareImage (v2). + """ + with open(filename, 'rb') as f: + if chip.lower() == 'esp32': + return ESP32FirmwareImage(f) + else: # Otherwise, ESP8266 so look at magic to determine the image type + magic = ord(f.read(1)) + f.seek(0) + if magic == ESPLoader.ESP_IMAGE_MAGIC: + return ESP8266ROMFirmwareImage(f) + elif magic == ESPBOOTLOADER.IMAGE_V2_MAGIC: + return ESP8266V2FirmwareImage(f) + else: + raise FatalError("Invalid image magic number: %d" % magic) + + +class ImageSegment(object): + """ Wrapper class for a segment in an ESP image + (very similar to a section in an ELFImage also) """ + def __init__(self, addr, data, file_offs=None): + self.addr = addr + self.data = data + self.file_offs = file_offs + self.include_in_checksum = True + if self.addr != 0: + self.pad_to_alignment(4) # pad all "real" ImageSegments 4 byte aligned length + + def copy_with_new_addr(self, new_addr): + """ Return a new ImageSegment with same data, but mapped at + a new address. """ + return ImageSegment(new_addr, self.data, 0) + + def split_image(self, split_len): + """ Return a new ImageSegment which splits "split_len" bytes + from the beginning of the data. Remaining bytes are kept in + this segment object (and the start address is adjusted to match.) """ + result = copy.copy(self) + result.data = self.data[:split_len] + self.data = self.data[split_len:] + self.addr += split_len + self.file_offs = None + result.file_offs = None + return result + + def __repr__(self): + r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr) + if self.file_offs is not None: + r += " file_offs 0x%08x" % (self.file_offs) + return r + + def pad_to_alignment(self, alignment): + self.data = pad_to(self.data, alignment, b'\x00') + + +class ELFSection(ImageSegment): + """ Wrapper class for a section in an ELF image, has a section + name as well as the common properties of an ImageSegment. """ + def __init__(self, name, addr, data): + super(ELFSection, self).__init__(addr, data) + self.name = name.decode("utf-8") + + def __repr__(self): + return "%s %s" % (self.name, super(ELFSection, self).__repr__()) + + +class BaseFirmwareImage(object): + SEG_HEADER_LEN = 8 + SHA256_DIGEST_LEN = 32 + + """ Base class with common firmware image functions """ + def __init__(self): + self.segments = [] + self.entrypoint = 0 + self.elf_sha256 = None + self.elf_sha256_offset = 0 + + def load_common_header(self, load_file, expected_magic): + (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' 16: + raise FatalError('Invalid segment count %d (max 16). Usually this indicates a linker script problem.' % len(self.segments)) + + def load_segment(self, f, is_irom_segment=False): + """ Load the next segment from the image file """ + file_offs = f.tell() + (offset, size) = struct.unpack(' 0x40200000 or offset < 0x3ffe0000 or size > 65536: + print('WARNING: Suspicious segment 0x%x, length %d' % (offset, size)) + + def maybe_patch_segment_data(self, f, segment_data): + """If SHA256 digest of the ELF file needs to be inserted into this segment, do so. Returns segment data.""" + segment_len = len(segment_data) + file_pos = f.tell() + if self.elf_sha256_offset >= file_pos and self.elf_sha256_offset < file_pos + segment_len: + # SHA256 digest needs to be patched into this segment, + # calculate offset of the digest inside the segment. + patch_offset = self.elf_sha256_offset - file_pos + # Sanity checks + if patch_offset < self.SEG_HEADER_LEN or patch_offset + self.SHA256_DIGEST_LEN > segment_len: + raise FatalError('Can not place SHA256 digest on segment boundary' + + '(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)' % + (self.elf_sha256_offset, file_pos, segment_len)) + assert(len(self.elf_sha256) == self.SHA256_DIGEST_LEN) + # offset relative to the data part + patch_offset -= self.SEG_HEADER_LEN + segment_data = segment_data[0:patch_offset] + self.elf_sha256 + \ + segment_data[patch_offset + self.SHA256_DIGEST_LEN:] + return segment_data + + def save_segment(self, f, segment, checksum=None): + """ Save the next segment to the image file, return next checksum value if provided """ + segment_data = self.maybe_patch_segment_data(f, segment.data) + f.write(struct.pack(' 0: + if len(irom_segments) != 1: + raise FatalError('Found %d segments that could be irom0. Bad ELF file?' % len(irom_segments)) + return irom_segments[0] + return None + + def get_non_irom_segments(self): + irom_segment = self.get_irom_segment() + return [s for s in self.segments if s != irom_segment] + + +class ESP8266ROMFirmwareImage(BaseFirmwareImage): + """ 'Version 1' firmware image, segments loaded directly by the ROM bootloader. """ + + ROM_LOADER = ESP8266ROM + + def __init__(self, load_file=None): + super(ESP8266ROMFirmwareImage, self).__init__() + self.flash_mode = 0 + self.flash_size_freq = 0 + self.version = 1 + + if load_file is not None: + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + return input_file + '-' + + def save(self, basename): + """ Save a set of V1 images for flashing. Parameter is a base filename. """ + # IROM data goes in its own plain binary file + irom_segment = self.get_irom_segment() + if irom_segment is not None: + with open("%s0x%05x.bin" % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START), "wb") as f: + f.write(irom_segment.data) + + # everything but IROM goes at 0x00000 in an image file + normal_segments = self.get_non_irom_segments() + with open("%s0x00000.bin" % basename, 'wb') as f: + self.write_common_header(f, normal_segments) + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + for segment in normal_segments: + checksum = self.save_segment(f, segment, checksum) + self.append_checksum(f, checksum) + + +class ESP8266V2FirmwareImage(BaseFirmwareImage): + """ 'Version 2' firmware image, segments loaded by software bootloader stub + (ie Espressif bootloader or rboot) + """ + + ROM_LOADER = ESP8266ROM + + def __init__(self, load_file=None): + super(ESP8266V2FirmwareImage, self).__init__() + self.version = 2 + if load_file is not None: + segments = self.load_common_header(load_file, ESPBOOTLOADER.IMAGE_V2_MAGIC) + if segments != ESPBOOTLOADER.IMAGE_V2_SEGMENT: + # segment count is not really segment count here, but we expect to see '4' + print('Warning: V2 header has unexpected "segment" count %d (usually 4)' % segments) + + # irom segment comes before the second header + # + # the file is saved in the image with a zero load address + # in the header, so we need to calculate a load address + irom_segment = self.load_segment(load_file, True) + irom_segment.addr = 0 # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8 + irom_segment.include_in_checksum = False + + first_flash_mode = self.flash_mode + first_flash_size_freq = self.flash_size_freq + first_entrypoint = self.entrypoint + # load the second header + + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + if first_flash_mode != self.flash_mode: + print('WARNING: Flash mode value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' + % (first_flash_mode, self.flash_mode)) + if first_flash_size_freq != self.flash_size_freq: + print('WARNING: Flash size/freq value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' + % (first_flash_size_freq, self.flash_size_freq)) + if first_entrypoint != self.entrypoint: + print('WARNING: Entrypoint address in first header (0x%08x) disagrees with second header (0x%08x). Using second value.' + % (first_entrypoint, self.entrypoint)) + + # load all the usual segments + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + irom_segment = self.get_irom_segment() + if irom_segment is not None: + irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START + else: + irom_offs = 0 + return "%s-0x%05x.bin" % (os.path.splitext(input_file)[0], + irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) + + def save(self, filename): + with open(filename, 'wb') as f: + # Save first header for irom0 segment + f.write(struct.pack(b' 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % + (segment.addr, last_addr)) + last_addr = segment.addr + + def get_alignment_data_needed(segment): + # Actual alignment (in data bytes) required for a segment header: positioned so that + # after we write the next 8 byte header, file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN + # + # (this is because the segment's vaddr may not be IROM_ALIGNed, more likely is aligned + # IROM_ALIGN+0x18 to account for the binary file header + align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN + pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past + if pad_len == 0 or pad_len == self.IROM_ALIGN: + return 0 # already aligned + + # subtract SEG_HEADER_LEN a second time, as the padding block has a header as well + pad_len -= self.SEG_HEADER_LEN + if pad_len < 0: + pad_len += self.IROM_ALIGN + return pad_len + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + pad_len = get_alignment_data_needed(segment) + if pad_len > 0: # need to pad + if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN: + pad_segment = ram_segments[0].split_image(pad_len) + if len(ram_segments[0].data) == 0: + ram_segments.pop(0) + else: + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + else: + # write the flash segment + assert (f.tell() + 8) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN + checksum = self.save_flash_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + if self.secure_pad: + # pad the image so that after signing it will end on a a 64KB boundary. + # This ensures all mapped flash content will be verified. + if not self.append_digest: + raise FatalError("secure_pad only applies if a SHA-256 digest is also appended to the image") + align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN + # 16 byte aligned checksum (force the alignment to simplify calculations) + checksum_space = 16 + # after checksum: SHA-256 digest + (to be added by signing process) version, signature + 12 trailing bytes due to alignment + space_after_checksum = 32 + 4 + 64 + 12 + pad_len = (self.IROM_ALIGN - align_past - checksum_space - space_after_checksum) % self.IROM_ALIGN + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + if self.secure_pad: + assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0 + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + try: + f.write(chr(total_segments)) + except TypeError: # Python 3 + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + with open(filename, 'wb') as real_file: + real_file.write(f.getvalue()) + + def save_flash_segment(self, f, segment, checksum=None): + """ Save the next segment to the image file, return next checksum value if provided """ + segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN + segment_len_remainder = segment_end_pos % self.IROM_ALIGN + if segment_len_remainder < 0x24: + # Work around a bug in ESP-IDF 2nd stage bootloader, that it didn't map the + # last MMU page, if an IROM/DROM segment was < 0x24 bytes over the page boundary. + segment.data += b'\x00' * (0x24 - segment_len_remainder) + return self.save_segment(f, segment, checksum) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + if fields[15] in [0, 1]: + self.append_digest = (fields[15] == 1) + else: + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15]) + + # remaining fields in the middle should all be zero + if any(f for f in fields[4:15] if f != 0): + print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") + + def save_extended_header(self, save_file): + def join_byte(ln,hn): + return (ln & 0x0F) + ((hn & 0x0F) << 4) + + append_digest = 1 if self.append_digest else 0 + + fields = [self.wp_pin, + join_byte(self.clk_drv, self.q_drv), + join_byte(self.d_drv, self.cs_drv), + join_byte(self.hd_drv, self.wp_drv)] + fields += [0] * 11 + fields += [append_digest] + + packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields) + save_file.write(packed) + + +class ELFFile(object): + SEC_TYPE_PROGBITS = 0x01 + SEC_TYPE_STRTAB = 0x03 + + LEN_SEC_HEADER = 0x28 + + def __init__(self, name): + # Load sections from the ELF file + self.name = name + with open(self.name, 'rb') as f: + self._read_elf_file(f) + + def get_section(self, section_name): + for s in self.sections: + if s.name == section_name: + return s + raise ValueError("No section %s in ELF file" % section_name) + + def _read_elf_file(self, f): + # read the ELF file header + LEN_FILE_HEADER = 0x34 + try: + (ident,_type,machine,_version, + self.entrypoint,_phoff,shoff,_flags, + _ehsize, _phentsize,_phnum, shentsize, + shnum,shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) + except struct.error as e: + raise FatalError("Failed to read a valid ELF header from %s: %s" % (self.name, e)) + + if byte(ident, 0) != 0x7f or ident[1:4] != b'ELF': + raise FatalError("%s has invalid ELF magic header" % self.name) + if machine != 0x5e: + raise FatalError("%s does not appear to be an Xtensa ELF file. e_machine=%04x" % (self.name, machine)) + if shentsize != self.LEN_SEC_HEADER: + raise FatalError("%s has unexpected section header entry size 0x%x (not 0x28)" % (self.name, shentsize, self.LEN_SEC_HEADER)) + if shnum == 0: + raise FatalError("%s has 0 section headers" % (self.name)) + self._read_sections(f, shoff, shnum, shstrndx) + + def _read_sections(self, f, section_header_offs, section_header_count, shstrndx): + f.seek(section_header_offs) + len_bytes = section_header_count * self.LEN_SEC_HEADER + section_header = f.read(len_bytes) + if len(section_header) == 0: + raise FatalError("No section header found at offset %04x in ELF file." % section_header_offs) + if len(section_header) != (len_bytes): + raise FatalError("Only read 0x%x bytes from section header (expected 0x%x.) Truncated ELF file?" % (len(section_header), len_bytes)) + + # walk through the section header and extract all sections + section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) + + def read_section_header(offs): + name_offs,sec_type,_flags,lma,sec_offs,size = struct.unpack_from(" 0] + self.sections = prog_sections + + def sha256(self): + # return SHA256 hash of the input ELF file + sha256 = hashlib.sha256() + with open(self.name, 'rb') as f: + sha256.update(f.read()) + return sha256.digest() + + +def slip_reader(port, trace_function): + """Generator to read SLIP packets from a serial port. + Yields one full SLIP packet at a time, raises exception on timeout or invalid data. + + Designed to avoid too many calls to serial.read(1), which can bog + down on slow systems. + """ + partial_packet = None + in_escape = False + while True: + waiting = port.inWaiting() + read_bytes = port.read(1 if waiting == 0 else waiting) + if read_bytes == b'': + waiting_for = "header" if partial_packet is None else "content" + trace_function("Timed out waiting for packet %s", waiting_for) + raise FatalError("Timed out waiting for packet %s" % waiting_for) + trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes)) + for b in read_bytes: + if type(b) is int: + b = bytes([b]) # python 2/3 compat + + if partial_packet is None: # waiting for packet header + if b == b'\xc0': + partial_packet = b"" + else: + trace_function("Read invalid data: %s", HexFormatter(read_bytes)) + trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) + raise FatalError('Invalid head of packet (0x%s)' % hexify(b)) + elif in_escape: # part-way through escape sequence + in_escape = False + if b == b'\xdc': + partial_packet += b'\xc0' + elif b == b'\xdd': + partial_packet += b'\xdb' + else: + trace_function("Read invalid data: %s", HexFormatter(read_bytes)) + trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) + raise FatalError('Invalid SLIP escape (0xdb, 0x%s)' % (hexify(b))) + elif b == b'\xdb': # start of escape sequence + in_escape = True + elif b == b'\xc0': # end of packet + trace_function("Received full packet: %s", HexFormatter(partial_packet)) + yield partial_packet + partial_packet = None + else: # normal byte in packet + partial_packet += b + + +def arg_auto_int(x): + return int(x, 0) + + +def div_roundup(a, b): + """ Return a/b rounded up to nearest integer, + equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only + without possible floating point accuracy errors. + """ + return (int(a) + int(b) - 1) // int(b) + + +def align_file_position(f, size): + """ Align the position in the file to the next block of specified size """ + align = (size - 1) - (f.tell() % size) + f.seek(align, 1) + + +def flash_size_bytes(size): + """ Given a flash size of the type passed in args.flash_size + (ie 512KB or 1MB) then return the size in bytes. + """ + if "MB" in size: + return int(size[:size.index("MB")]) * 1024 * 1024 + elif "KB" in size: + return int(size[:size.index("KB")]) * 1024 + else: + raise FatalError("Unknown size %s" % size) + + +def hexify(s, uppercase=True): + format_str = '%02X' if uppercase else '%02x' + if not PYTHON2: + return ''.join(format_str % c for c in s) + else: + return ''.join(format_str % ord(c) for c in s) + + +class HexFormatter(object): + """ + Wrapper class which takes binary data in its constructor + and returns a hex string as it's __str__ method. + + This is intended for "lazy formatting" of trace() output + in hex format. Avoids overhead (significant on slow computers) + of generating long hex strings even if tracing is disabled. + + Note that this doesn't save any overhead if passed as an + argument to "%", only when passed to trace() + + If auto_split is set (default), any long line (> 16 bytes) will be + printed as separately indented lines, with ASCII decoding at the end + of each line. + """ + def __init__(self, binary_string, auto_split=True): + self._s = binary_string + self._auto_split = auto_split + + def __str__(self): + if self._auto_split and len(self._s) > 16: + result = "" + s = self._s + while len(s) > 0: + line = s[:16] + ascii_line = "".join(c if (c == ' ' or (c in string.printable and c not in string.whitespace)) + else '.' for c in line.decode('ascii', 'replace')) + s = s[16:] + result += "\n %-16s %-16s | %s" % (hexify(line[:8], False), hexify(line[8:], False), ascii_line) + return result + else: + return hexify(self._s, False) + + +def pad_to(data, alignment, pad_character=b'\xFF'): + """ Pad to the next alignment boundary """ + pad_mod = len(data) % alignment + if pad_mod != 0: + data += pad_character * (alignment - pad_mod) + return data + + +class FatalError(RuntimeError): + """ + Wrapper class for runtime errors that aren't caused by internal bugs, but by + ESP8266 responses or input content. + """ + def __init__(self, message): + RuntimeError.__init__(self, message) + + @staticmethod + def WithResult(message, result): + """ + Return a fatal error object that appends the hex values of + 'result' as a string formatted argument. + """ + message += " (result was %s)" % hexify(result) + return FatalError(message) + + +class NotImplementedInROMError(FatalError): + """ + Wrapper class for the error thrown when a particular ESP bootloader function + is not implemented in the ROM bootloader. + """ + def __init__(self, bootloader, func): + FatalError.__init__(self, "%s ROM does not support function %s." % (bootloader.CHIP_NAME, func.__name__)) + + +class NotSupportedError(FatalError): + def __init__(self, esp, function_name): + FatalError.__init__(self, "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME)) + +# "Operation" commands, executable at command line. One function each +# +# Each function takes either two args (, ) or a single +# argument. + + +def load_ram(esp, args): + image = LoadFirmwareImage(esp.CHIP_NAME, args.filename) + + print('RAM boot...') + for seg in image.segments: + size = len(seg.data) + print('Downloading %d bytes at %08x...' % (size, seg.addr), end=' ') + sys.stdout.flush() + esp.mem_begin(size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr) + + seq = 0 + while len(seg.data) > 0: + esp.mem_block(seg.data[0:esp.ESP_RAM_BLOCK], seq) + seg.data = seg.data[esp.ESP_RAM_BLOCK:] + seq += 1 + print('done!') + + print('All segments done, executing at %08x' % image.entrypoint) + esp.mem_finish(image.entrypoint) + + +def read_mem(esp, args): + print('0x%08x = 0x%08x' % (args.address, esp.read_reg(args.address))) + + +def write_mem(esp, args): + esp.write_reg(args.address, args.value, args.mask, 0) + print('Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address)) + + +def dump_mem(esp, args): + with open(args.filename, 'wb') as f: + for i in range(args.size // 4): + d = esp.read_reg(args.address + (i * 4)) + f.write(struct.pack(b'> 16 + args.flash_size = DETECTED_FLASH_SIZES.get(size_id) + if args.flash_size is None: + print('Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x), defaulting to 4MB' % (flash_id, size_id)) + args.flash_size = '4MB' + else: + print('Auto-detected Flash size:', args.flash_size) + + +def _update_image_flash_params(esp, address, args, image): + """ Modify the flash mode & size bytes if this looks like an executable bootloader image """ + if len(image) < 8: + return image # not long enough to be a bootloader image + + # unpack the (potential) image header + magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4]) + if address != esp.BOOTLOADER_FLASH_OFFSET or magic != esp.ESP_IMAGE_MAGIC: + return image # not flashing a bootloader, so don't modify this + + if args.flash_mode != 'keep': + flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] + + flash_freq = flash_size_freq & 0x0F + if args.flash_freq != 'keep': + flash_freq = {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] + + flash_size = flash_size_freq & 0xF0 + if args.flash_size != 'keep': + flash_size = esp.parse_flash_size_arg(args.flash_size) + + flash_params = struct.pack(b'BB', flash_mode, flash_size + flash_freq) + if flash_params != image[2:4]: + print('Flash params set to 0x%04x' % struct.unpack(">H", flash_params)) + image = image[0:2] + flash_params + image[4:] + return image + + +def write_flash(esp, args): + # set args.compress based on default behaviour: + # -> if either --compress or --no-compress is set, honour that + # -> otherwise, set --compress unless --no-stub is set + if args.compress is None and not args.no_compress: + args.compress = not args.no_stub + + # verify file sizes fit in flash + flash_end = flash_size_bytes(args.flash_size) + for address, argfile in args.addr_filename: + argfile.seek(0,2) # seek to end + if address + argfile.tell() > flash_end: + raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " + + "Use --flash-size argument, or change flashing address.") + % (argfile.name, argfile.tell(), address, flash_end)) + argfile.seek(0) + + if args.erase_all: + erase_flash(esp, args) + + for address, argfile in args.addr_filename: + if args.no_stub: + print('Erasing flash...') + image = pad_to(argfile.read(), 4) + if len(image) == 0: + print('WARNING: File %s is empty' % argfile.name) + continue + image = _update_image_flash_params(esp, address, args, image) + calcmd5 = hashlib.md5(image).hexdigest() + uncsize = len(image) + if args.compress: + uncimage = image + image = zlib.compress(uncimage, 9) + ratio = uncsize / len(image) + blocks = esp.flash_defl_begin(uncsize, len(image), address) + else: + ratio = 1.0 + blocks = esp.flash_begin(uncsize, address) + argfile.seek(0) # in case we need it again + seq = 0 + written = 0 + t = time.time() + while len(image) > 0: + print('\rWriting at 0x%08x... (%d %%)' % (address + seq * esp.FLASH_WRITE_SIZE, 100 * (seq + 1) // blocks), end='') + sys.stdout.flush() + block = image[0:esp.FLASH_WRITE_SIZE] + if args.compress: + esp.flash_defl_block(block, seq, timeout=DEFAULT_TIMEOUT * ratio * 2) + else: + # Pad the last block + block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block)) + esp.flash_block(block, seq) + image = image[esp.FLASH_WRITE_SIZE:] + seq += 1 + written += len(block) + t = time.time() - t + speed_msg = "" + if args.compress: + if t > 0.0: + speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) + print('\rWrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, written, address, t, speed_msg)) + else: + if t > 0.0: + speed_msg = " (%.1f kbit/s)" % (written / t * 8 / 1000) + print('\rWrote %d bytes at 0x%08x in %.1f seconds%s...' % (written, address, t, speed_msg)) + try: + res = esp.flash_md5sum(address, uncsize) + if res != calcmd5: + print('File md5: %s' % calcmd5) + print('Flash md5: %s' % res) + print('MD5 of 0xFF is %s' % (hashlib.md5(b'\xFF' * uncsize).hexdigest())) + raise FatalError("MD5 of file does not match data in flash!") + else: + print('Hash of data verified.') + except NotImplementedInROMError: + pass + + print('\nLeaving...') + + if esp.IS_STUB: + # skip sending flash_finish to ROM loader here, + # as it causes the loader to exit and run user code + esp.flash_begin(0, 0) + if args.compress: + esp.flash_defl_finish(False) + else: + esp.flash_finish(False) + + if args.verify: + print('Verifying just-written flash...') + print('(This option is deprecated, flash contents are now always read back after flashing.)') + verify_flash(esp, args) + + +def image_info(args): + image = LoadFirmwareImage(args.chip, args.filename) + print('Image version: %d' % image.version) + print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set') + print('%d segments' % len(image.segments)) + print + idx = 0 + for seg in image.segments: + idx += 1 + print('Segment %d: %r' % (idx, seg)) + calc_checksum = image.calculate_checksum() + print('Checksum: %02x (%s)' % (image.checksum, + 'valid' if image.checksum == calc_checksum else 'invalid - calculated %02x' % calc_checksum)) + try: + digest_msg = 'Not appended' + if image.append_digest: + is_valid = image.stored_digest == image.calc_digest + digest_msg = "%s (%s)" % (hexify(image.calc_digest).lower(), + "valid" if is_valid else "invalid") + print('Validation Hash: %s' % digest_msg) + except AttributeError: + pass # ESP8266 image has no append_digest field + + +def make_image(args): + image = ESP8266ROMFirmwareImage() + if len(args.segfile) == 0: + raise FatalError('No segments specified') + if len(args.segfile) != len(args.segaddr): + raise FatalError('Number of specified files does not match number of specified addresses') + for (seg, addr) in zip(args.segfile, args.segaddr): + with open(seg, 'rb') as f: + data = f.read() + image.segments.append(ImageSegment(addr, data)) + image.entrypoint = args.entrypoint + image.save(args.output) + + +def elf2image(args): + e = ELFFile(args.input) + if args.chip == 'auto': # Default to ESP8266 for backwards compatibility + print("Creating image for ESP8266...") + args.chip = 'esp8266' + + if args.chip == 'esp32': + image = ESP32FirmwareImage() + image.secure_pad = args.secure_pad + elif args.version == '1': # ESP8266 + image = ESP8266ROMFirmwareImage() + else: + image = ESP8266V2FirmwareImage() + image.entrypoint = e.entrypoint + image.segments = e.sections # ELFSection is a subclass of ImageSegment + image.flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] + image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size] + image.flash_size_freq += {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] + + if args.elf_sha256_offset: + image.elf_sha256 = e.sha256() + image.elf_sha256_offset = args.elf_sha256_offset + + image.verify() + + if args.output is None: + args.output = image.default_output_name(args.input) + image.save(args.output) + + +def read_mac(esp, args): + mac = esp.read_mac() + + def print_mac(label, mac): + print('%s: %s' % (label, ':'.join(map(lambda x: '%02x' % x, mac)))) + print_mac("MAC", mac) + + +def chip_id(esp, args): + try: + chipid = esp.chip_id() + print('Chip ID: 0x%08x' % chipid) + except NotSupportedError: + print('Warning: %s has no Chip ID. Reading MAC instead.' % esp.CHIP_NAME) + read_mac(esp, args) + + +def erase_flash(esp, args): + print('Erasing flash (this may take a while)...') + t = time.time() + esp.erase_flash() + print('Chip erase completed successfully in %.1fs' % (time.time() - t)) + + +def erase_region(esp, args): + print('Erasing region (may be slow depending on size)...') + t = time.time() + esp.erase_region(args.address, args.size) + print('Erase completed successfully in %.1f seconds.' % (time.time() - t)) + + +def run(esp, args): + esp.run() + + +def flash_id(esp, args): + flash_id = esp.flash_id() + print('Manufacturer: %02x' % (flash_id & 0xff)) + flid_lowbyte = (flash_id >> 16) & 0xFF + print('Device: %02x%02x' % ((flash_id >> 8) & 0xff, flid_lowbyte)) + print('Detected flash size: %s' % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))) + + +def read_flash(esp, args): + if args.no_progress: + flash_progress = None + else: + def flash_progress(progress, length): + msg = '%d (%d %%)' % (progress, progress * 100.0 / length) + padding = '\b' * len(msg) + if progress == length: + padding = '\n' + sys.stdout.write(msg + padding) + sys.stdout.flush() + t = time.time() + data = esp.read_flash(args.address, args.size, flash_progress) + t = time.time() - t + print('\rRead %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' + % (len(data), args.address, t, len(data) / t * 8 / 1000)) + with open(args.filename, 'wb') as f: + f.write(data) + + +def verify_flash(esp, args): + differences = False + + for address, argfile in args.addr_filename: + image = pad_to(argfile.read(), 4) + argfile.seek(0) # rewind in case we need it again + + image = _update_image_flash_params(esp, address, args, image) + + image_size = len(image) + print('Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % (image_size, image_size, address, argfile.name)) + # Try digest first, only read if there are differences. + digest = esp.flash_md5sum(address, image_size) + expected_digest = hashlib.md5(image).hexdigest() + if digest == expected_digest: + print('-- verify OK (digest matched)') + continue + else: + differences = True + if getattr(args, 'diff', 'no') != 'yes': + print('-- verify FAILED (digest mismatch)') + continue + + flash = esp.read_flash(address, image_size) + assert flash != image + diff = [i for i in range(image_size) if flash[i] != image[i]] + print('-- verify FAILED: %d differences, first @ 0x%08x' % (len(diff), address + diff[0])) + for d in diff: + flash_byte = flash[d] + image_byte = image[d] + if PYTHON2: + flash_byte = ord(flash_byte) + image_byte = ord(image_byte) + print(' %08x %02x %02x' % (address + d, flash_byte, image_byte)) + if differences: + raise FatalError("Verify failed.") + + +def read_flash_status(esp, args): + print('Status value: 0x%04x' % esp.read_status(args.bytes)) + + +def write_flash_status(esp, args): + fmt = "0x%%0%dx" % (args.bytes * 2) + args.value = args.value & ((1 << (args.bytes * 8)) - 1) + print(('Initial flash status: ' + fmt) % esp.read_status(args.bytes)) + print(('Setting flash status: ' + fmt) % args.value) + esp.write_status(args.value, args.bytes, args.non_volatile) + print(('After flash status: ' + fmt) % esp.read_status(args.bytes)) + + +def version(args): + print(__version__) + +# +# End of operations functions +# + + +def main(custom_commandline=None): + """ + Main function for esptool + + custom_commandline - Optional override for default arguments parsing (that uses sys.argv), can be a list of custom arguments + as strings. + """ + parser = argparse.ArgumentParser(description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool') + + parser.add_argument('--chip', '-c', + help='Target chip type', + choices=['auto', 'esp8266', 'esp32'], + default=os.environ.get('ESPTOOL_CHIP', 'auto')) + + parser.add_argument( + '--port', '-p', + help='Serial port device', + default=os.environ.get('ESPTOOL_PORT', None)) + + parser.add_argument( + '--baud', '-b', + help='Serial port baud rate used when flashing/reading', + type=arg_auto_int, + default=os.environ.get('ESPTOOL_BAUD', ESPLoader.ESP_ROM_BAUD)) + + parser.add_argument( + '--before', + help='What to do before connecting to the chip', + choices=['default_reset', 'no_reset', 'no_reset_no_sync'], + default=os.environ.get('ESPTOOL_BEFORE', 'default_reset')) + + parser.add_argument( + '--after', '-a', + help='What to do after esptool.py is finished', + choices=['hard_reset', 'soft_reset', 'no_reset'], + default=os.environ.get('ESPTOOL_AFTER', 'hard_reset')) + + parser.add_argument( + '--no-stub', + help="Disable launching the flasher stub, only talk to ROM bootloader. Some features will not be available.", + action='store_true') + + parser.add_argument( + '--trace', '-t', + help="Enable trace-level output of esptool.py interactions.", + action='store_true') + + parser.add_argument( + '--override-vddsdio', + help="Override ESP32 VDDSDIO internal voltage regulator (use with care)", + choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES, + nargs='?') + + subparsers = parser.add_subparsers( + dest='operation', + help='Run esptool {command} -h for additional help') + + def add_spi_connection_arg(parent): + parent.add_argument('--spi-connection', '-sc', help='ESP32-only argument. Override default SPI Flash connection. ' + + 'Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers to use for SPI flash (CLK,Q,D,HD,CS).', + action=SpiConnectionAction) + + parser_load_ram = subparsers.add_parser( + 'load_ram', + help='Download an image to RAM and execute') + parser_load_ram.add_argument('filename', help='Firmware image') + + parser_dump_mem = subparsers.add_parser( + 'dump_mem', + help='Dump arbitrary memory to disk') + parser_dump_mem.add_argument('address', help='Base address', type=arg_auto_int) + parser_dump_mem.add_argument('size', help='Size of region to dump', type=arg_auto_int) + parser_dump_mem.add_argument('filename', help='Name of binary dump') + + parser_read_mem = subparsers.add_parser( + 'read_mem', + help='Read arbitrary memory location') + parser_read_mem.add_argument('address', help='Address to read', type=arg_auto_int) + + parser_write_mem = subparsers.add_parser( + 'write_mem', + help='Read-modify-write to arbitrary memory location') + parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int) + parser_write_mem.add_argument('value', help='Value', type=arg_auto_int) + parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int) + + def add_spi_flash_subparsers(parent, is_elf2image): + """ Add common parser arguments for SPI flash properties """ + extra_keep_args = [] if is_elf2image else ['keep'] + auto_detect = not is_elf2image + + parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', + choices=extra_keep_args + ['40m', '26m', '20m', '80m'], + default=os.environ.get('ESPTOOL_FF', '40m' if is_elf2image else 'keep')) + parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', + choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], + default=os.environ.get('ESPTOOL_FM', 'qio' if is_elf2image else 'keep')) + parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)' + ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)', + action=FlashSizeAction, auto_detect=auto_detect, + default=os.environ.get('ESPTOOL_FS', 'detect' if auto_detect else '1MB')) + add_spi_connection_arg(parent) + + parser_write_flash = subparsers.add_parser('write_flash', help='Write a binary blob to flash') + parser_write_flash.add_argument('addr_filename', metavar='
', help='Address followed by binary filename, separated by space', + action=AddrFilenamePairAction) + parser_write_flash.add_argument('--erase-all', '-e', + help='Erase all regions of flash (not just write areas) before programming', + action="store_true") + + add_spi_flash_subparsers(parser_write_flash, is_elf2image=False) + parser_write_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") + parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' + + '(mostly superfluous, data is read back during flashing)', action='store_true') + + compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) + compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)',action="store_true", default=None) + compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)',action="store_true") + + subparsers.add_parser( + 'run', + help='Run application code in flash') + + parser_image_info = subparsers.add_parser( + 'image_info', + help='Dump headers from an application image') + parser_image_info.add_argument('filename', help='Image file to parse') + + parser_make_image = subparsers.add_parser( + 'make_image', + help='Create an application image from binary files') + parser_make_image.add_argument('output', help='Output image file') + parser_make_image.add_argument('--segfile', '-f', action='append', help='Segment input file') + parser_make_image.add_argument('--segaddr', '-a', action='append', help='Segment base address', type=arg_auto_int) + parser_make_image.add_argument('--entrypoint', '-e', help='Address of entry point', type=arg_auto_int, default=0) + + parser_elf2image = subparsers.add_parser( + 'elf2image', + help='Create an application image from ELF file') + parser_elf2image.add_argument('input', help='Input ELF file') + parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) + parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1','2'], default='1') + parser_elf2image.add_argument('--secure-pad', action='store_true', help='Pad image so once signed it will end on a 64KB boundary. For ESP32 images only.') + parser_elf2image.add_argument('--elf-sha256-offset', help='If set, insert SHA256 hash (32 bytes) of the input ELF file at specified offset in the binary.', + type=arg_auto_int, default=None) + + add_spi_flash_subparsers(parser_elf2image, is_elf2image=True) + + subparsers.add_parser( + 'read_mac', + help='Read MAC address from OTP ROM') + + subparsers.add_parser( + 'chip_id', + help='Read Chip ID from OTP ROM') + + parser_flash_id = subparsers.add_parser( + 'flash_id', + help='Read SPI flash manufacturer and device ID') + add_spi_connection_arg(parser_flash_id) + + parser_read_status = subparsers.add_parser( + 'read_flash_status', + help='Read SPI flash status register') + + add_spi_connection_arg(parser_read_status) + parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1,2,3], default=2) + + parser_write_status = subparsers.add_parser( + 'write_flash_status', + help='Write SPI flash status register') + + add_spi_connection_arg(parser_write_status) + parser_write_status.add_argument('--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') + parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1,2,3], default=2) + parser_write_status.add_argument('value', help='New value', type=arg_auto_int) + + parser_read_flash = subparsers.add_parser( + 'read_flash', + help='Read SPI flash content') + add_spi_connection_arg(parser_read_flash) + parser_read_flash.add_argument('address', help='Start address', type=arg_auto_int) + parser_read_flash.add_argument('size', help='Size of region to dump', type=arg_auto_int) + parser_read_flash.add_argument('filename', help='Name of binary dump') + parser_read_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") + + parser_verify_flash = subparsers.add_parser( + 'verify_flash', + help='Verify a binary blob against flash') + parser_verify_flash.add_argument('addr_filename', help='Address and binary file to verify there, separated by space', + action=AddrFilenamePairAction) + parser_verify_flash.add_argument('--diff', '-d', help='Show differences', + choices=['no', 'yes'], default='no') + add_spi_flash_subparsers(parser_verify_flash, is_elf2image=False) + + parser_erase_flash = subparsers.add_parser( + 'erase_flash', + help='Perform Chip Erase on SPI flash') + add_spi_connection_arg(parser_erase_flash) + + parser_erase_region = subparsers.add_parser( + 'erase_region', + help='Erase a region of the flash') + add_spi_connection_arg(parser_erase_region) + parser_erase_region.add_argument('address', help='Start address (must be multiple of 4096)', type=arg_auto_int) + parser_erase_region.add_argument('size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) + + subparsers.add_parser( + 'version', help='Print esptool version') + + # internal sanity check - every operation matches a module function of the same name + for operation in subparsers.choices.keys(): + assert operation in globals(), "%s should be a module function" % operation + + expand_file_arguments() + + args = parser.parse_args(custom_commandline) + + print('esptool.py v%s' % __version__) + + # operation function can take 1 arg (args), 2 args (esp, arg) + # or be a member function of the ESPLoader class. + + if args.operation is None: + parser.print_help() + sys.exit(1) + + operation_func = globals()[args.operation] + + if PYTHON2: + # This function is depreciated in Python3 + operation_args = inspect.getargspec(operation_func).args + else: + operation_args = inspect.getfullargspec(operation_func).args + + if operation_args[0] == 'esp': # operation function takes an ESPLoader connection object + if args.before != "no_reset_no_sync": + initial_baud = min(ESPLoader.ESP_ROM_BAUD, args.baud) # don't sync faster than the default baud rate + else: + initial_baud = args.baud + + if args.port is None: + ser_list = sorted(ports.device for ports in list_ports.comports()) + print("Found %d serial ports" % len(ser_list)) + else: + ser_list = [args.port] + esp = None + for each_port in reversed(ser_list): + print("Serial port %s" % each_port) + try: + if args.chip == 'auto': + esp = ESPLoader.detect_chip(each_port, initial_baud, args.before, args.trace) + else: + chip_class = { + 'esp8266': ESP8266ROM, + 'esp32': ESP32ROM, + }[args.chip] + esp = chip_class(each_port, initial_baud, args.trace) + esp.connect(args.before) + break + except (FatalError, OSError) as err: + if args.port is not None: + raise + print("%s failed to connect: %s" % (each_port, err)) + esp = None + if esp is None: + raise FatalError("All of the %d available serial ports could not connect to a Espressif device." % len(ser_list)) + + print("Chip is %s" % (esp.get_chip_description())) + + print("Features: %s" % ", ".join(esp.get_chip_features())) + + read_mac(esp, args) + + if not args.no_stub: + esp = esp.run_stub() + + if args.override_vddsdio: + esp.override_vddsdio(args.override_vddsdio) + + if args.baud > initial_baud: + try: + esp.change_baud(args.baud) + except NotImplementedInROMError: + print("WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d" % initial_baud) + + # override common SPI flash parameter stuff if configured to do so + if hasattr(args, "spi_connection") and args.spi_connection is not None: + if esp.CHIP_NAME != "ESP32": + raise FatalError("Chip %s does not support --spi-connection option." % esp.CHIP_NAME) + print("Configuring SPI flash mode...") + esp.flash_spi_attach(args.spi_connection) + elif args.no_stub: + print("Enabling default SPI flash mode...") + # ROM loader doesn't enable flash unless we explicitly do it + esp.flash_spi_attach(0) + + if hasattr(args, "flash_size"): + print("Configuring flash size...") + detect_flash_size(esp, args) + esp.flash_set_parameters(flash_size_bytes(args.flash_size)) + + try: + operation_func(esp, args) + finally: + try: # Clean up AddrFilenamePairAction files + for address, argfile in args.addr_filename: + argfile.close() + except AttributeError: + pass + + # Handle post-operation behaviour (reset or other) + if operation_func == load_ram: + # the ESP is now running the loaded image, so let it run + print('Exiting immediately.') + elif args.after == 'hard_reset': + print('Hard resetting via RTS pin...') + esp.hard_reset() + elif args.after == 'soft_reset': + print('Soft resetting...') + # flash_finish will trigger a soft reset + esp.soft_reset(False) + else: + print('Staying in bootloader.') + if esp.IS_STUB: + esp.soft_reset(True) # exit stub back to ROM loader + + esp._port.close() + + else: + operation_func(args) + + +def expand_file_arguments(): + """ Any argument starting with "@" gets replaced with all values read from a text file. + Text file arguments can be split by newline or by space. + Values are added "as-is", as if they were specified in this order on the command line. + """ + new_args = [] + expanded = False + for arg in sys.argv: + if arg.startswith("@"): + expanded = True + with open(arg[1:],"r") as f: + for line in f.readlines(): + new_args += shlex.split(line) + else: + new_args.append(arg) + if expanded: + print("esptool.py %s" % (" ".join(new_args[1:]))) + sys.argv = new_args + + +class FlashSizeAction(argparse.Action): + """ Custom flash size parser class to support backwards compatibility with megabit size arguments. + + (At next major relase, remove deprecated sizes and this can become a 'normal' choices= argument again.) + """ + def __init__(self, option_strings, dest, nargs=1, auto_detect=False, **kwargs): + super(FlashSizeAction, self).__init__(option_strings, dest, nargs, **kwargs) + self._auto_detect = auto_detect + + def __call__(self, parser, namespace, values, option_string=None): + try: + value = { + '2m': '256KB', + '4m': '512KB', + '8m': '1MB', + '16m': '2MB', + '32m': '4MB', + '16m-c1': '2MB-c1', + '32m-c1': '4MB-c1', + }[values[0]] + print("WARNING: Flash size arguments in megabits like '%s' are deprecated." % (values[0])) + print("Please use the equivalent size '%s'." % (value)) + print("Megabit arguments may be removed in a future release.") + except KeyError: + value = values[0] + + known_sizes = dict(ESP8266ROM.FLASH_SIZES) + known_sizes.update(ESP32ROM.FLASH_SIZES) + if self._auto_detect: + known_sizes['detect'] = 'detect' + if value not in known_sizes: + raise argparse.ArgumentError(self, '%s is not a known flash size. Known sizes: %s' % (value, ", ".join(known_sizes.keys()))) + setattr(namespace, self.dest, value) + + +class SpiConnectionAction(argparse.Action): + """ Custom action to parse 'spi connection' override. Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas. + """ + def __call__(self, parser, namespace, value, option_string=None): + if value.upper() == "SPI": + value = 0 + elif value.upper() == "HSPI": + value = 1 + elif "," in value: + values = value.split(",") + if len(values) != 5: + raise argparse.ArgumentError(self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) + try: + values = tuple(int(v,0) for v in values) + except ValueError: + raise argparse.ArgumentError(self, '%s is not a valid argument. All pins must be numeric values' % values) + if any([v for v in values if v > 33 or v < 0]): + raise argparse.ArgumentError(self, 'Pin numbers must be in the range 0-33.') + # encode the pin numbers as a 32-bit integer with packed 6-bit values, the same way ESP32 ROM takes them + # todo: make this less ESP32 ROM specific somehow... + clk,q,d,hd,cs = values + value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk + else: + raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' + + 'Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS).' % value) + setattr(namespace, self.dest, value) + + +class AddrFilenamePairAction(argparse.Action): + """ Custom parser class for the address/filename pairs passed as arguments """ + def __init__(self, option_strings, dest, nargs='+', **kwargs): + super(AddrFilenamePairAction, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + # validate pair arguments + pairs = [] + for i in range(0,len(values),2): + try: + address = int(values[i],0) + except ValueError: + raise argparse.ArgumentError(self,'Address "%s" must be a number' % values[i]) + try: + argfile = open(values[i + 1], 'rb') + except IOError as e: + raise argparse.ArgumentError(self, e) + except IndexError: + raise argparse.ArgumentError(self,'Must be pairs of an address and the binary filename to write there') + pairs.append((address, argfile)) + + # Sort the addresses and check for overlapping + end = 0 + for address, argfile in sorted(pairs): + argfile.seek(0,2) # seek to end + size = argfile.tell() + argfile.seek(0) + sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) + sector_end = ((address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) - 1 + if sector_start < end: + message = 'Detected overlap at address: 0x%x for file: %s' % (address, argfile.name) + raise argparse.ArgumentError(self, message) + end = sector_end + setattr(namespace, self.dest, pairs) + + +# Binary stub code (see flasher_stub dir for source & details) +ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) + + +def _main(): + try: + main() + except FatalError as e: + print('\nA fatal error occurred: %s' % e) + sys.exit(2) + + +if __name__ == '__main__': + _main() diff --git a/scripts/main_script.py b/scripts/main_script.py old mode 100644 new mode 100755 diff --git a/scripts/memanalyzer.py b/scripts/memanalyzer.py old mode 100644 new mode 100755 diff --git a/scripts/ota.sh b/scripts/ota.sh deleted file mode 100644 index d9e8a9b5f..000000000 --- a/scripts/ota.sh +++ /dev/null @@ -1,4 +0,0 @@ -# python espota.py -i 10.10.10.189 --port 8267 --auth neo -f ../.pio/build/debug/firmware.bin - -python espota.py --debug --progress -i 10.10.10.100 -f ../.pio/build/debug/firmware.bin - diff --git a/scripts/partitions.bin b/scripts/partitions.bin new file mode 100644 index 000000000..fa042d671 Binary files /dev/null and b/scripts/partitions.bin differ diff --git a/scripts/upload_esp32.py b/scripts/upload_esp32.py new file mode 100755 index 000000000..7afd4b095 --- /dev/null +++ b/scripts/upload_esp32.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import subprocess +import os, argparse + +print("\n** Starting upload...") + +ap = argparse.ArgumentParser() +ap.add_argument("-p", "--port", required=True, help="port") +args = vars(ap.parse_args()) + +# esptool.py --chip esp32 --port "COM4" --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 bootloader_dio_40m.bin 0x8000 partitions.bin 0xe000 boot_app0.bin 0x10000 EMS-ESP-dev-esp32.bin + +subprocess.call(["esptool.py", "--chip esp32", "-p", args['port'], "--baud", "921600", "--before", "default_reset", "--after", "hard_reset", "write_flash", "-z", "--flash_mode", "dio", "--flash_freq", "40m","--flash_size", "detect", "0x1000", "bootloader_dio_40m.bin", "0x8000", "partitions.bin","0xe000", "boot_app0.bin", "0x10000", "EMS-ESP-dev-esp32.bin"]) + +print("\n** Finished upload.") diff --git a/src/EMSESPDevicesService.cpp b/src/EMSESPDevicesService.cpp index 40896ae7d..a914d89c0 100644 --- a/src/EMSESPDevicesService.cpp +++ b/src/EMSESPDevicesService.cpp @@ -81,7 +81,7 @@ void EMSESPDevicesService::device_data(AsyncWebServerRequest * request, JsonVari AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_DEVICE_SIZE); #ifndef EMSESP_STANDALONE uint8_t id = json["id"]; // get id from selected table row - EMSESP::device_info(id, (JsonObject &)response->getRoot()); + EMSESP::device_info_web(id, (JsonObject &)response->getRoot()); #endif response->setLength(); request->send(response); diff --git a/src/EMSESPDevicesService.h b/src/EMSESPDevicesService.h index 6d058dcd2..5d3d23158 100644 --- a/src/EMSESPDevicesService.h +++ b/src/EMSESPDevicesService.h @@ -24,7 +24,7 @@ #include #include -#define MAX_EMSESP_DEVICE_SIZE 1536 +#define MAX_EMSESP_DEVICE_SIZE 1700 #define EMSESP_DEVICES_SERVICE_PATH "/rest/allDevices" #define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices" diff --git a/src/EMSESPSettingsService.cpp b/src/EMSESPSettingsService.cpp index 4be387282..cf4cbf018 100644 --- a/src/EMSESPSettingsService.cpp +++ b/src/EMSESPSettingsService.cpp @@ -36,11 +36,12 @@ void EMSESPSettings::read(EMSESPSettings & settings, JsonObject & root) { root["master_thermostat"] = settings.master_thermostat; root["shower_timer"] = settings.shower_timer; root["shower_alert"] = settings.shower_alert; - root["hide_led"] = settings.hide_led; root["rx_gpio"] = settings.rx_gpio; root["tx_gpio"] = settings.tx_gpio; root["dallas_gpio"] = settings.dallas_gpio; + root["dallas_parasite"] = settings.dallas_parasite; root["led_gpio"] = settings.led_gpio; + root["hide_led"] = settings.hide_led; } StateUpdateResult EMSESPSettings::update(JsonObject & root, EMSESPSettings & settings) { @@ -52,11 +53,12 @@ StateUpdateResult EMSESPSettings::update(JsonObject & root, EMSESPSettings & set settings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT; settings.shower_timer = root["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER; settings.shower_alert = root["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT; - settings.hide_led = root["hide_led"] | EMSESP_DEFAULT_HIDE_LED; settings.rx_gpio = root["rx_gpio"] | EMSESP_DEFAULT_RX_GPIO; settings.tx_gpio = root["tx_gpio"] | EMSESP_DEFAULT_TX_GPIO; settings.dallas_gpio = root["dallas_gpio"] | EMSESP_DEFAULT_DALLAS_GPIO; + settings.dallas_parasite = root["dallas_parasite"] | EMSESP_DEFAULT_DALLAS_PARASITE; settings.led_gpio = root["led_gpio"] | EMSESP_DEFAULT_LED_GPIO; + settings.hide_led = root["hide_led"] | EMSESP_DEFAULT_HIDE_LED; return StateUpdateResult::CHANGED; } @@ -68,6 +70,8 @@ void EMSESPSettingsService::onUpdate() { // EMSESP::system_.syslog_init(); // changing SysLog will require a restart EMSESP::init_tx(); System::set_led(); + Sensors sensors_; // Dallas sensors + sensors_.start(); } void EMSESPSettingsService::begin() { diff --git a/src/EMSESPSettingsService.h b/src/EMSESPSettingsService.h index 113833538..a23d344ca 100644 --- a/src/EMSESPSettingsService.h +++ b/src/EMSESPSettingsService.h @@ -34,6 +34,7 @@ #define EMSESP_DEFAULT_SHOWER_TIMER false #define EMSESP_DEFAULT_SHOWER_ALERT false #define EMSESP_DEFAULT_HIDE_LED false +#define EMSESP_DEFAULT_DALLAS_PARASITE false // Default GPIO PIN definitions #if defined(ESP8266) @@ -65,14 +66,15 @@ class EMSESPSettings { uint8_t master_thermostat; bool shower_timer; bool shower_alert; - bool hide_led; int8_t syslog_level; // uuid::log::Level uint32_t syslog_mark_interval; String syslog_host; uint8_t rx_gpio; uint8_t tx_gpio; uint8_t dallas_gpio; + bool dallas_parasite; uint8_t led_gpio; + bool hide_led; static void read(EMSESPSettings & settings, JsonObject & root); static StateUpdateResult update(JsonObject & root, EMSESPSettings & settings); diff --git a/src/console.cpp b/src/console.cpp index 0878ca24f..5809ff19f 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -88,9 +88,13 @@ void EMSESPShell::display_banner() { // load the list of commands add_console_commands(); - // turn off watch + // turn off watch, unless is test mode emsesp::EMSESP::watch_id(WATCH_ID_NONE); +#if defined(EMSESP_STANDALONE) + emsesp::EMSESP::watch(EMSESP::WATCH_ON); +#else emsesp::EMSESP::watch(EMSESP::WATCH_OFF); +#endif } // pre-loads all the console commands into the MAIN context @@ -240,6 +244,17 @@ void EMSESPShell::add_console_commands() { }); }); + commands->add_command(ShellContext::MAIN, + CommandFlags::ADMIN, + flash_string_vector{F_(read)}, + flash_string_vector{F_(deviceid_mandatory), F_(typeid_mandatory)}, + [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { + uint8_t device_id = Helpers::hextoint(arguments.front().c_str()); + uint16_t type_id = Helpers::hextoint(arguments.back().c_str()); + EMSESP::set_read_id(type_id); + EMSESP::send_read_request(type_id, device_id); + }); + /* * add all the submenu contexts... */ @@ -393,7 +408,7 @@ void Console::load_standard_commands(unsigned int context) { flash_string_vector{F_(watch)}, flash_string_vector{F_(watch_format_optional), F_(watchid_optional)}, [](Shell & shell, const std::vector & arguments) { - uint16_t watch_id; + uint16_t watch_id = WATCH_ID_NONE; if (!arguments.empty()) { // get raw/pretty @@ -403,16 +418,16 @@ void Console::load_standard_commands(unsigned int context) { emsesp::EMSESP::watch(EMSESP::WATCH_ON); // on } else if (arguments[0] == read_flash_string(F_(off))) { emsesp::EMSESP::watch(EMSESP::WATCH_OFF); // off - } else { + } else if (emsesp::EMSESP::watch() == EMSESP::WATCH_OFF) { shell.printfln(F_(invalid_watch)); return; + } else { + watch_id = Helpers::hextoint(arguments[0].c_str()); } if (arguments.size() == 2) { // get the watch_id if its set watch_id = Helpers::hextoint(arguments[1].c_str()); - } else { - watch_id = WATCH_ID_NONE; } emsesp::EMSESP::watch_id(watch_id); @@ -436,7 +451,9 @@ void Console::load_standard_commands(unsigned int context) { } watch_id = emsesp::EMSESP::watch_id(); - if (watch_id != WATCH_ID_NONE) { + if (watch_id > 0x80) { + shell.printfln(F("Filtering only telegrams that match a telegram type of 0x%02X"), watch_id); + } else if (watch_id != WATCH_ID_NONE) { shell.printfln(F("Filtering only telegrams that match a device ID or telegram type of 0x%02X"), watch_id); } }); @@ -526,6 +543,7 @@ std::string EMSESPStreamConsole::console_name() { } // Start up telnet and logging +// Log order is off, err, warning, notice, info, debug, trace, all void Console::start() { // if we've detected a boot into safe mode on ESP8266, start the Serial console too // Serial is always on with the ESP32 as it has 2 UARTs @@ -540,10 +558,15 @@ void Console::start() { #ifndef ESP8266 #if defined(EMSESP_DEBUG) - shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all + shell->log_level(uuid::log::Level::DEBUG); #endif #endif +#if defined(EMSESP_FORCE_SERIAL) + shell->log_level(uuid::log::Level::DEBUG); +#endif + + #if defined(EMSESP_STANDALONE) // always start in su/admin mode when running tests shell->add_flags(CommandFlags::ADMIN); @@ -558,7 +581,7 @@ void Console::start() { #endif // turn watch off in case it was still set in the last session - emsesp::EMSESP::watch(EMSESP::WATCH_OFF); + // emsesp::EMSESP::watch(EMSESP::WATCH_OFF); } // handles telnet sync and logging to console diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 48874f457..d092c1803 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -39,7 +39,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const register_telegram_type(0x1C, F("UBAMaintenanceStatus"), false, [&](std::shared_ptr t) { process_UBAMaintenanceStatus(t); }); register_telegram_type(0x2A, F("MC10Status"), false, [&](std::shared_ptr t) { process_MC10Status(t); }); register_telegram_type(0x33, F("UBAParameterWW"), true, [&](std::shared_ptr t) { process_UBAParameterWW(t); }); - register_telegram_type(0x14, F("UBATotalUptime"), false, [&](std::shared_ptr t) { process_UBATotalUptime(t); }); + register_telegram_type(0x14, F("UBATotalUptime"), true, [&](std::shared_ptr t) { process_UBATotalUptime(t); }); register_telegram_type(0x35, F("UBAFlags"), false, [&](std::shared_ptr t) { process_UBAFlags(t); }); register_telegram_type(0x15, F("UBAMaintenanceData"), false, [&](std::shared_ptr t) { process_UBAMaintenanceData(t); }); register_telegram_type(0x16, F("UBAParameters"), true, [&](std::shared_ptr t) { process_UBAParameters(t); }); @@ -64,6 +64,14 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const register_mqtt_cmd(F("boilhystoff"), [&](const char * value, const int8_t id) { set_hyst_off(value, id); }); register_mqtt_cmd(F("burnperiod"), [&](const char * value, const int8_t id) { set_burn_period(value, id); }); register_mqtt_cmd(F("pumpdelay"), [&](const char * value, const int8_t id) { set_pump_delay(value, id); }); + + EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { + mqtt_format_ = settings.mqtt_format; // single, nested or ha + + if (mqtt_format_ == MQTT_format::HA) { + register_mqtt_ha_config(); + } + }); } // add submenu context @@ -77,9 +85,36 @@ void Boiler::add_context_menu() { }); } -void Boiler::device_info(JsonArray & root) { +// create the config topic for Home Assistant MQTT Discovery +// homeassistant/sensor/ems-esp/boiler +// state is /state +// config is /config +void Boiler::register_mqtt_ha_config() { + StaticJsonDocument doc; + + /* + * not finished yet - see https://github.com/proddy/EMS-ESP/issues/288 + + doc["name"] = "boiler"; + doc["uniq_id"] = "boiler"; + + // Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA + Mqtt::publish("homeassistant/sensor/ems-esp/boiler/config", doc, true); // publish the config payload with retain flag + + */ +} + +void Boiler::device_info_web(JsonArray & root) { JsonObject dataElement; + if (serviceCodeChar_[0] && Helpers::hasValue(serviceCode_)) { + dataElement = root.createNestedObject(); + dataElement["name"] = F("Service Code"); + char s[12]; + snprintf_P(s, 12, PSTR("%s (%d)"), serviceCodeChar_, serviceCode_); + dataElement["value"] = s; + } + if (Helpers::hasValue(tap_water_active_, EMS_VALUE_BOOL)) { dataElement = root.createNestedObject(); dataElement["name"] = F("Hot tap water"); @@ -98,6 +133,8 @@ void Boiler::device_info(JsonArray & root) { render_value_json(root, "", F("Warm Water set temperature"), wWSetTmp_, F_(degrees)); render_value_json(root, "", F("Warm Water current temperature (intern)"), wWCurTmp_, F_(degrees), 10); render_value_json(root, "", F("Warm Water current temperature (extern)"), wWCurTmp2_, F_(degrees), 10); + render_value_json(root, "", F("Pump modulation"), pumpMod_, F_(percent)); + render_value_json(root, "", F("Heat Pump modulation"), pumpMod2_, F_(percent)); } // publish values via MQTT @@ -196,8 +233,8 @@ void Boiler::publish_values() { if (Helpers::hasValue(wWOneTime_, EMS_VALUE_BOOL)) { doc["wWOnetime"] = Helpers::render_value(s, wWOneTime_, EMS_VALUE_BOOL); } - if (Helpers::hasValue(wWDesinfecting_, EMS_VALUE_BOOL)) { - doc["wWDesinfecting"] = Helpers::render_value(s, wWDesinfecting_, EMS_VALUE_BOOL); + if (Helpers::hasValue(wWDisinfecting_, EMS_VALUE_BOOL)) { + doc["wWDisinfecting"] = Helpers::render_value(s, wWDisinfecting_, EMS_VALUE_BOOL); } if (Helpers::hasValue(wWReadiness_, EMS_VALUE_BOOL)) { doc["wWReady"] = Helpers::render_value(s, wWReadiness_, EMS_VALUE_BOOL); @@ -287,12 +324,16 @@ void Boiler::publish_values() { // if we have data, publish it if (!doc.isNull()) { - Mqtt::publish("boiler_data", doc); + Mqtt::publish(F("boiler_data"), doc); } } // called after a process command is called, to check values and see if we need to force an MQTT publish bool Boiler::updated_values() { + if (changed_) { + changed_ = false; + return true; + } return false; } @@ -353,7 +394,7 @@ void Boiler::show_values(uuid::console::Shell & shell) { shell.printfln(F(" Warm Water active time: %d days %d hours %d minutes"), wWWorkM_ / 1440, (wWWorkM_ % 1440) / 60, wWWorkM_ % 60); } print_value(shell, 2, F("Warm Water charging"), wWHeat_, nullptr, EMS_VALUE_BOOL); - print_value(shell, 2, F("Warm Water disinfecting"), wWDesinfecting_, nullptr, EMS_VALUE_BOOL); + print_value(shell, 2, F("Warm Water disinfecting"), wWDisinfecting_, nullptr, EMS_VALUE_BOOL); print_value(shell, 2, F("Selected flow temperature"), selFlowTemp_, F_(degrees)); print_value(shell, 2, F("Current flow temperature"), curFlowTemp_, F_(degrees), 10); print_value(shell, 2, F("Max boiler temperature"), boilTemp_, F_(degrees), 10); @@ -396,7 +437,7 @@ void Boiler::show_values(uuid::console::Shell & shell) { print_value(shell, 2, F("Exhaust temperature"), exhaustTemp_, F_(degrees), 10); print_value(shell, 2, F("Pump modulation"), pumpMod_, F_(percent)); - print_value(shell, 2, F("Pump modulation2"), pumpMod2_, F_(percent)); + print_value(shell, 2, F("Heat Pump modulation"), pumpMod2_, F_(percent)); print_value(shell, 2, F("Burner # starts"), burnStarts_, nullptr); if (Helpers::hasValue(burnWorkMin_)) { shell.printfln(F(" Total burner operating time: %d days %d hours %d minutes"), burnWorkMin_ / 1440, (burnWorkMin_ % 1440) / 60, burnWorkMin_ % 60); @@ -435,54 +476,54 @@ void Boiler::check_active() { uint8_t latest_boilerState = (tap_water_active_ << 1) + heating_active_; if (latest_boilerState != last_boilerState) { last_boilerState = latest_boilerState; - Mqtt::publish("tapwater_active", tap_water_active_); - Mqtt::publish("heating_active", heating_active_); + Mqtt::publish(F("tapwater_active"), tap_water_active_); + Mqtt::publish(F("heating_active"), heating_active_); } } } // 0x33 void Boiler::process_UBAParameterWW(std::shared_ptr telegram) { - telegram->read_value(wWActivated_, 1); // 0xFF means on - telegram->read_value(wWCircPump_, 6); // 0xFF means on - telegram->read_value(wWCircPumpMode_, 7); // 1=1x3min... 6=6x3min, 7=continuous - telegram->read_value(wWCircPumpType_, 10); // 0 = charge pump, 0xff = 3-way valve - telegram->read_value(wWSelTemp_, 2); - telegram->read_value(wWDisinfectTemp_, 8); - telegram->read_value(wWComfort_, 9); + changed_ |= telegram->read_value(wWActivated_, 1); // 0xFF means on + changed_ |= telegram->read_value(wWCircPump_, 6); // 0xFF means on + changed_ |= telegram->read_value(wWCircPumpMode_, 7); // 1=1x3min... 6=6x3min, 7=continuous + changed_ |= telegram->read_value(wWCircPumpType_, 10); // 0 = charge pump, 0xff = 3-way valve + changed_ |= telegram->read_value(wWSelTemp_, 2); + changed_ |= telegram->read_value(wWDisinfectTemp_, 8); + changed_ |= telegram->read_value(wWComfort_, 9); } // 0x18 void Boiler::process_UBAMonitorFast(std::shared_ptr telegram) { - telegram->read_value(selFlowTemp_, 0); - telegram->read_value(curFlowTemp_, 1); - telegram->read_value(selBurnPow_, 3); // burn power max setting - telegram->read_value(curBurnPow_, 4); + changed_ |= telegram->read_value(selFlowTemp_, 0); + changed_ |= telegram->read_value(curFlowTemp_, 1); + changed_ |= telegram->read_value(selBurnPow_, 3); // burn power max setting + changed_ |= telegram->read_value(curBurnPow_, 4); - telegram->read_bitvalue(burnGas_, 7, 0); - telegram->read_bitvalue(fanWork_, 7, 2); - telegram->read_bitvalue(ignWork_, 7, 3); - telegram->read_bitvalue(heatPmp_, 7, 5); - telegram->read_bitvalue(wWHeat_, 7, 6); - telegram->read_bitvalue(wWCirc_, 7, 7); + changed_ |= telegram->read_bitvalue(burnGas_, 7, 0); + changed_ |= telegram->read_bitvalue(fanWork_, 7, 2); + changed_ |= telegram->read_bitvalue(ignWork_, 7, 3); + changed_ |= telegram->read_bitvalue(heatPmp_, 7, 5); + changed_ |= telegram->read_bitvalue(wWHeat_, 7, 6); + changed_ |= telegram->read_bitvalue(wWCirc_, 7, 7); // warm water storage sensors (if present) // wwStorageTemp2 is also used by some brands as the boiler temperature - see https://github.com/proddy/EMS-ESP/issues/206 - telegram->read_value(wwStorageTemp1_, 9); // 0x8300 if not available - telegram->read_value(wwStorageTemp2_, 11); // 0x8000 if not available - this is boiler temp + changed_ |= telegram->read_value(wwStorageTemp1_, 9); // 0x8300 if not available + changed_ |= telegram->read_value(wwStorageTemp2_, 11); // 0x8000 if not available - this is boiler temp - telegram->read_value(retTemp_, 13); - telegram->read_value(flameCurr_, 15); - telegram->read_value(serviceCode_, 20); + changed_ |= telegram->read_value(retTemp_, 13); + changed_ |= telegram->read_value(flameCurr_, 15); + changed_ |= telegram->read_value(serviceCode_, 20); // system pressure. FF means missing - telegram->read_value(sysPress_, 17); // is *10 + changed_ |= telegram->read_value(sysPress_, 17); // is *10 // read the service code / installation status as appears on the display if ((telegram->message_length > 18) && (telegram->offset == 0)) { - serviceCodeChar_[0] = char(telegram->message_data[18]); // ascii character 1 - serviceCodeChar_[1] = char(telegram->message_data[19]); // ascii character 2 - serviceCodeChar_[2] = '\0'; // null terminate string + changed_ |= telegram->read_value(serviceCodeChar_[0], 18); + changed_ |= telegram->read_value(serviceCodeChar_[1], 19); + serviceCodeChar_[2] = '\0'; // null terminate string } // at this point do a quick check to see if the hot water or heating is active @@ -494,22 +535,22 @@ void Boiler::process_UBAMonitorFast(std::shared_ptr telegram) { * received only after requested (not broadcasted) */ void Boiler::process_UBATotalUptime(std::shared_ptr telegram) { - telegram->read_value(UBAuptime_, 0, 3); // force to 3 bytes + changed_ |= telegram->read_value(UBAuptime_, 0, 3); // force to 3 bytes } /* * UBAParameters - type 0x16 */ void Boiler::process_UBAParameters(std::shared_ptr telegram) { - telegram->read_value(heating_temp_, 1); - telegram->read_value(burnPowermax_, 2); - telegram->read_value(burnPowermin_, 3); - telegram->read_value(boilTemp_off_, 4); - telegram->read_value(boilTemp_on_, 5); - telegram->read_value(burnPeriod_, 6); - telegram->read_value(pumpDelay_, 8); - telegram->read_value(pump_mod_max_, 9); - telegram->read_value(pump_mod_min_, 10); + changed_ |= telegram->read_value(heating_temp_, 1); + changed_ |= telegram->read_value(burnPowermax_, 2); + changed_ |= telegram->read_value(burnPowermin_, 3); + changed_ |= telegram->read_value(boilTemp_off_, 4); + changed_ |= telegram->read_value(boilTemp_on_, 5); + changed_ |= telegram->read_value(burnPeriod_, 6); + changed_ |= telegram->read_value(pumpDelay_, 8); + changed_ |= telegram->read_value(pump_mod_max_, 9); + changed_ |= telegram->read_value(pump_mod_min_, 10); } /* @@ -517,19 +558,19 @@ void Boiler::process_UBAParameters(std::shared_ptr telegram) { * received every 10 seconds */ void Boiler::process_UBAMonitorWW(std::shared_ptr telegram) { - telegram->read_value(wWSetTmp_, 0); - telegram->read_value(wWCurTmp_, 1); - telegram->read_value(wWCurTmp2_, 3); - telegram->read_value(wWCurFlow_, 9); + changed_ |= telegram->read_value(wWSetTmp_, 0); + changed_ |= telegram->read_value(wWCurTmp_, 1); + changed_ |= telegram->read_value(wWCurTmp2_, 3); + changed_ |= telegram->read_value(wWCurFlow_, 9); - telegram->read_value(wWWorkM_, 10, 3); // force to 3 bytes - telegram->read_value(wWStarts_, 13, 3); // force to 3 bytes + changed_ |= telegram->read_value(wWWorkM_, 10, 3); // force to 3 bytes + changed_ |= telegram->read_value(wWStarts_, 13, 3); // force to 3 bytes - telegram->read_bitvalue(wWOneTime_, 5, 1); - telegram->read_bitvalue(wWDesinfecting_, 5, 2); - telegram->read_bitvalue(wWReadiness_, 5, 3); - telegram->read_bitvalue(wWRecharging_, 5, 4); - telegram->read_bitvalue(wWTemperatureOK_, 5, 5); + changed_ |= telegram->read_bitvalue(wWOneTime_, 5, 1); + changed_ |= telegram->read_bitvalue(wWDisinfecting_, 5, 2); + changed_ |= telegram->read_bitvalue(wWReadiness_, 5, 3); + changed_ |= telegram->read_bitvalue(wWRecharging_, 5, 4); + changed_ |= telegram->read_bitvalue(wWTemperatureOK_, 5, 5); } /* @@ -537,18 +578,18 @@ void Boiler::process_UBAMonitorWW(std::shared_ptr telegram) { * Still to figure out are: serviceCode, retTemp, sysPress */ void Boiler::process_UBAMonitorFastPlus(std::shared_ptr telegram) { - telegram->read_value(selFlowTemp_, 6); - telegram->read_bitvalue(burnGas_, 11, 0); - telegram->read_bitvalue(wWHeat_, 11, 2); - telegram->read_value(curBurnPow_, 10); - telegram->read_value(selBurnPow_, 9); - telegram->read_value(curFlowTemp_, 7); - telegram->read_value(flameCurr_, 19); + changed_ |= telegram->read_value(selFlowTemp_, 6); + changed_ |= telegram->read_bitvalue(burnGas_, 11, 0); + changed_ |= telegram->read_bitvalue(wWHeat_, 11, 2); + changed_ |= telegram->read_value(curBurnPow_, 10); + changed_ |= telegram->read_value(selBurnPow_, 9); + changed_ |= telegram->read_value(curFlowTemp_, 7); + changed_ |= telegram->read_value(flameCurr_, 19); // read the service code / installation status as appears on the display if ((telegram->message_length > 4) && (telegram->offset == 0)) { - serviceCodeChar_[0] = char(telegram->message_data[4]); // ascii character 1 - serviceCodeChar_[1] = char(telegram->message_data[5]); // ascii character 2 + changed_ |= telegram->read_value(serviceCodeChar_[0], 4); + changed_ |= telegram->read_value(serviceCodeChar_[1], 5); serviceCodeChar_[2] = '\0'; } @@ -564,79 +605,79 @@ void Boiler::process_UBAMonitorFastPlus(std::shared_ptr telegram * 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 17 19 20 21 22 23 24 */ void Boiler::process_UBAMonitorSlow(std::shared_ptr telegram) { - telegram->read_value(extTemp_, 0); - telegram->read_value(boilTemp_, 2); - telegram->read_value(exhaustTemp_, 4); - telegram->read_value(switchTemp_, 25); // only if there is a mixing module present - telegram->read_value(pumpMod_, 9); - telegram->read_value(burnStarts_, 10, 3); // force to 3 bytes - telegram->read_value(burnWorkMin_, 13, 3); // force to 3 bytes - telegram->read_value(heatWorkMin_, 19, 3); // force to 3 bytes + changed_ |= telegram->read_value(extTemp_, 0); + changed_ |= telegram->read_value(boilTemp_, 2); + changed_ |= telegram->read_value(exhaustTemp_, 4); + changed_ |= telegram->read_value(switchTemp_, 25); // only if there is a mixing module present + changed_ |= telegram->read_value(pumpMod_, 9); + changed_ |= telegram->read_value(burnStarts_, 10, 3); // force to 3 bytes + changed_ |= telegram->read_value(burnWorkMin_, 13, 3); // force to 3 bytes + changed_ |= telegram->read_value(heatWorkMin_, 19, 3); // force to 3 bytes } /* * UBAMonitorSlowPlus2 - type 0xE3 */ void Boiler::process_UBAMonitorSlowPlus2(std::shared_ptr telegram) { - telegram->read_value(pumpMod2_, 13); + changed_ |= telegram->read_value(pumpMod2_, 13); // Heat Pump Modulation } /* * UBAMonitorSlowPlus - type 0xE5 - central heating monitor EMS+ */ void Boiler::process_UBAMonitorSlowPlus(std::shared_ptr telegram) { - telegram->read_bitvalue(fanWork_, 2, 2); - telegram->read_bitvalue(ignWork_, 2, 3); - telegram->read_bitvalue(heatPmp_, 2, 5); - telegram->read_bitvalue(wWCirc_, 2, 7); - telegram->read_value(burnStarts_, 10, 3); // force to 3 bytes - telegram->read_value(burnWorkMin_, 13, 3); // force to 3 bytes - telegram->read_value(heatWorkMin_, 19, 3); // force to 3 bytes - telegram->read_value(pumpMod_, 25); + changed_ |= telegram->read_bitvalue(fanWork_, 2, 2); + changed_ |= telegram->read_bitvalue(ignWork_, 2, 3); + changed_ |= telegram->read_bitvalue(heatPmp_, 2, 5); + changed_ |= telegram->read_bitvalue(wWCirc_, 2, 7); + changed_ |= telegram->read_value(burnStarts_, 10, 3); // force to 3 bytes + changed_ |= telegram->read_value(burnWorkMin_, 13, 3); // force to 3 bytes + changed_ |= telegram->read_value(heatWorkMin_, 19, 3); // force to 3 bytes + changed_ |= telegram->read_value(pumpMod_, 25); } // 0xE9 - DHW Status // e.g. 08 00 E9 00 37 01 F6 01 ED 00 00 00 00 41 3C 00 00 00 00 00 00 00 00 00 00 00 00 37 00 00 00 (CRC=77) #data=27 void Boiler::process_UBADHWStatus(std::shared_ptr telegram) { - telegram->read_value(wWSetTmp_, 0); - telegram->read_value(wWCurTmp_, 1); - telegram->read_value(wWCurTmp2_, 3); + changed_ |= telegram->read_value(wWSetTmp_, 0); + changed_ |= telegram->read_value(wWCurTmp_, 1); + changed_ |= telegram->read_value(wWCurTmp2_, 3); - telegram->read_value(wWWorkM_, 17, 3); // force to 3 bytes - telegram->read_value(wWStarts_, 14, 3); // force to 3 bytes + changed_ |= telegram->read_value(wWWorkM_, 17, 3); // force to 3 bytes + changed_ |= telegram->read_value(wWStarts_, 14, 3); // force to 3 bytes - telegram->read_bitvalue(wWOneTime_, 12, 2); - telegram->read_bitvalue(wWDesinfecting_, 12, 3); - telegram->read_bitvalue(wWReadiness_, 12, 4); - telegram->read_bitvalue(wWRecharging_, 13, 4); - telegram->read_bitvalue(wWTemperatureOK_, 13, 5); - telegram->read_bitvalue(wWCircPump_, 13, 2); + changed_ |= telegram->read_bitvalue(wWOneTime_, 12, 2); + changed_ |= telegram->read_bitvalue(wWDisinfecting_, 12, 3); + changed_ |= telegram->read_bitvalue(wWReadiness_, 12, 4); + changed_ |= telegram->read_bitvalue(wWRecharging_, 13, 4); + changed_ |= telegram->read_bitvalue(wWTemperatureOK_, 13, 5); + changed_ |= telegram->read_bitvalue(wWCircPump_, 13, 2); - telegram->read_value(wWActivated_, 20); - telegram->read_value(wWSelTemp_, 10); - telegram->read_value(wWDisinfectTemp_, 9); + changed_ |= telegram->read_value(wWActivated_, 20); + changed_ |= telegram->read_value(wWSelTemp_, 10); + changed_ |= telegram->read_value(wWDisinfectTemp_, 9); } // 0x2A - MC10Status // e.g. 88 00 2A 00 00 00 00 00 00 00 00 00 D2 00 00 80 00 00 01 08 80 00 02 47 00 // see https://github.com/proddy/EMS-ESP/issues/397 void Boiler::process_MC10Status(std::shared_ptr telegram) { - telegram->read_value(wwMixTemperature_, 14); - telegram->read_value(wwBufferBoilerTemperature_, 18); + changed_ |= telegram->read_value(wwMixTemperature_, 14); + changed_ |= telegram->read_value(wwBufferBoilerTemperature_, 18); } /* * UBAOutdoorTemp - type 0xD1 - external temperature EMS+ */ void Boiler::process_UBAOutdoorTemp(std::shared_ptr telegram) { - telegram->read_value(extTemp_, 0); + changed_ |= telegram->read_value(extTemp_, 0); } // UBASetPoint 0x1A void Boiler::process_UBASetPoints(std::shared_ptr telegram) { - telegram->read_value(setFlowTemp_, 0); // boiler set temp from thermostat - telegram->read_value(setBurnPow_, 1); // max output power in % - telegram->read_value(setWWPumpPow_, 2); // ww pump speed/power? + changed_ |= telegram->read_value(setFlowTemp_, 0); // boiler set temp from thermostat + changed_ |= telegram->read_value(setBurnPow_, 1); // max output power in % + changed_ |= telegram->read_value(setWWPumpPow_, 2); // ww pump speed/power? } #pragma GCC diagnostic push @@ -683,8 +724,8 @@ void Boiler::set_warmwater_temp(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler warm water temperature to %d C"), v); - write_command(EMS_TYPE_UBAParameterWW, 2, v); - write_command(EMS_TYPE_UBAFlags, 3, v); // for i9000, see #397 + write_command(EMS_TYPE_UBAParameterWW, 2, v, EMS_TYPE_UBAParameterWW); + write_command(EMS_TYPE_UBAFlags, 3, v, EMS_TYPE_UBAParameterWW); // for i9000, see #397 } // flow temp @@ -695,7 +736,7 @@ void Boiler::set_flow_temp(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler flow temperature to %d C"), v); - write_command(EMS_TYPE_UBASetPoints, 0, v); + write_command(EMS_TYPE_UBASetPoints, 0, v, EMS_TYPE_UBASetPoints); } // set min boiler output @@ -705,7 +746,7 @@ void Boiler::set_min_power(const char * value, const int8_t id) { return; } LOG_INFO(F("Setting boiler min power to "), v); - write_command(EMS_TYPE_UBAParameters, 3, v); + write_command(EMS_TYPE_UBAParameters, 3, v, EMS_TYPE_UBAParameters); } // set max temp @@ -716,10 +757,10 @@ void Boiler::set_max_power(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler max power to %d C"), v); - write_command(EMS_TYPE_UBAParameters, 2, v); + write_command(EMS_TYPE_UBAParameters, 2, v, EMS_TYPE_UBAParameters); } -// set oiler on hysteresis +// set boiler on hysteresis void Boiler::set_hyst_on(const char * value, const int8_t id) { int v = 0; if (!Helpers::value2number(value, v)) { @@ -727,7 +768,7 @@ void Boiler::set_hyst_on(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler hysteresis on to %d C"), v); - write_command(EMS_TYPE_UBAParameters, 5, v); + write_command(EMS_TYPE_UBAParameters, 5, v, EMS_TYPE_UBAParameters); } // set boiler off hysteresis @@ -738,7 +779,7 @@ void Boiler::set_hyst_off(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler hysteresis off to %d C"), v); - write_command(EMS_TYPE_UBAParameters, 4, v); + write_command(EMS_TYPE_UBAParameters, 4, v, EMS_TYPE_UBAParameters); } // set min burner period @@ -749,7 +790,7 @@ void Boiler::set_burn_period(const char * value, const int8_t id) { } LOG_INFO(F("Setting burner min. period to %d min"), v); - write_command(EMS_TYPE_UBAParameters, 6, v); + write_command(EMS_TYPE_UBAParameters, 6, v, EMS_TYPE_UBAParameters); } // set pump delay @@ -760,7 +801,7 @@ void Boiler::set_pump_delay(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler pump delay to %d min"), v); - write_command(EMS_TYPE_UBAParameters, 8, v); + write_command(EMS_TYPE_UBAParameters, 8, v, EMS_TYPE_UBAParameters); } // note some boilers do not have this setting, than it's done by thermostat @@ -782,7 +823,7 @@ void Boiler::set_warmwater_mode(const char * value, const int8_t id) { } else { return; // do nothing } - write_command(EMS_TYPE_UBAParameterWW, 9, set); + write_command(EMS_TYPE_UBAParameterWW, 9, set, EMS_TYPE_UBAParameterWW); } // turn on/off warm water @@ -801,7 +842,7 @@ void Boiler::set_warmwater_activated(const char * value, const int8_t id) { } else { n = (v ? 0xFF : 0x00); // 0xFF is on, 0x00 is off } - write_command(EMS_TYPE_UBAParameterWW, 1, n); + write_command(EMS_TYPE_UBAParameterWW, 1, n, EMS_TYPE_UBAParameterWW); } // Activate / De-activate the Warm Tap Water @@ -846,7 +887,7 @@ void Boiler::set_warmwater_onetime(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler warm water OneTime loading %s"), v ? "on" : "off"); - write_command(EMS_TYPE_UBAFlags, 0, (v ? 0x22 : 0x02)); + write_command(EMS_TYPE_UBAFlags, 0, (v ? 0x22 : 0x02), 0x18); } // Activate / De-activate circulation of warm water 0x35 @@ -858,7 +899,7 @@ void Boiler::set_warmwater_circulation(const char * value, const int8_t id) { } LOG_INFO(F("Setting boiler warm water circulation %s"), v ? "on" : "off"); - write_command(EMS_TYPE_UBAFlags, 1, (v ? 0x22 : 0x02)); + write_command(EMS_TYPE_UBAFlags, 1, (v ? 0x22 : 0x02), 0x18); } // add console commands diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 7a22c0513..7ae907573 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -40,7 +40,7 @@ class Boiler : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); @@ -48,8 +48,12 @@ class Boiler : public EMSdevice { static uuid::log::Logger logger_; void console_commands(Shell & shell, unsigned int context); + void register_mqtt_ha_config(); + void check_active(); uint8_t last_boilerState = 0xFF; // remember last state of heating and warm water on/off + uint8_t mqtt_format_; // single, nested or ha + bool changed_ = false; static constexpr uint8_t EMS_TYPE_UBAParameterWW = 0x33; static constexpr uint8_t EMS_TYPE_UBAFunctionTest = 0x1D; @@ -108,7 +112,7 @@ class Boiler : public EMSdevice { uint32_t wWStarts_ = EMS_VALUE_ULONG_NOTSET; // Warm Water # starts uint32_t wWWorkM_ = EMS_VALUE_ULONG_NOTSET; // Warm Water # minutes uint8_t wWOneTime_ = EMS_VALUE_BOOL_NOTSET; // Warm Water one time function on/off - uint8_t wWDesinfecting_ = EMS_VALUE_BOOL_NOTSET; // Warm Water disinfection on/off + uint8_t wWDisinfecting_ = EMS_VALUE_BOOL_NOTSET; // Warm Water disinfection on/off uint8_t wWReadiness_ = EMS_VALUE_BOOL_NOTSET; // Warm Water readiness on/off uint8_t wWRecharging_ = EMS_VALUE_BOOL_NOTSET; // Warm Water recharge on/off uint8_t wWTemperatureOK_ = EMS_VALUE_BOOL_NOTSET; // Warm Water temperature ok on/off @@ -147,7 +151,6 @@ class Boiler : public EMSdevice { void process_UBAMonitorSlow(std::shared_ptr telegram); void process_UBAMonitorSlowPlus(std::shared_ptr telegram); void process_UBAMonitorSlowPlus2(std::shared_ptr telegram); - void process_UBAOutdoorTemp(std::shared_ptr telegram); void process_UBASetPoints(std::shared_ptr telegram); void process_UBAFlags(std::shared_ptr telegram); @@ -155,11 +158,8 @@ class Boiler : public EMSdevice { void process_UBAMaintenanceStatus(std::shared_ptr telegram); void process_UBAMaintenanceData(std::shared_ptr telegram); void process_UBAErrorMessage(std::shared_ptr telegram); - void process_UBADHWStatus(std::shared_ptr telegram); - void check_active(); - // commands - none of these use the additional id parameter void set_warmwater_mode(const char * value, const int8_t id); void set_warmwater_activated(const char * value, const int8_t id); diff --git a/src/devices/connect.cpp b/src/devices/connect.cpp index 60b8de5c3..029d9f0bd 100644 --- a/src/devices/connect.cpp +++ b/src/devices/connect.cpp @@ -28,7 +28,7 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { } -void Connect::device_info(JsonArray & root) { +void Connect::device_info_web(JsonArray & root) { } void Connect::add_context_menu() { diff --git a/src/devices/connect.h b/src/devices/connect.h index 51956ef5a..9e25f33eb 100644 --- a/src/devices/connect.h +++ b/src/devices/connect.h @@ -37,7 +37,7 @@ class Connect : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/controller.cpp b/src/devices/controller.cpp index 07bd9879c..23faaeac4 100644 --- a/src/devices/controller.cpp +++ b/src/devices/controller.cpp @@ -31,7 +31,7 @@ Controller::Controller(uint8_t device_type, uint8_t device_id, uint8_t product_i void Controller::add_context_menu() { } -void Controller::device_info(JsonArray & root) { +void Controller::device_info_web(JsonArray & root) { } // display all values into the shell console diff --git a/src/devices/controller.h b/src/devices/controller.h index d905b3864..a92931621 100644 --- a/src/devices/controller.h +++ b/src/devices/controller.h @@ -37,7 +37,7 @@ class Controller : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/gateway.cpp b/src/devices/gateway.cpp index e36e40c38..e52fc4e60 100644 --- a/src/devices/gateway.cpp +++ b/src/devices/gateway.cpp @@ -31,7 +31,7 @@ Gateway::Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, con void Gateway::add_context_menu() { } -void Gateway::device_info(JsonArray & root) { +void Gateway::device_info_web(JsonArray & root) { } // display all values into the shell console diff --git a/src/devices/gateway.h b/src/devices/gateway.h index d38dbb32e..df4b6f04b 100644 --- a/src/devices/gateway.h +++ b/src/devices/gateway.h @@ -37,7 +37,7 @@ class Gateway : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/heatpump.cpp b/src/devices/heatpump.cpp index 112a27963..1ffb8014b 100644 --- a/src/devices/heatpump.cpp +++ b/src/devices/heatpump.cpp @@ -37,7 +37,7 @@ Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, c void Heatpump::add_context_menu() { } -void Heatpump::device_info(JsonArray & root) { +void Heatpump::device_info_web(JsonArray & root) { } // display all values into the shell console diff --git a/src/devices/heatpump.h b/src/devices/heatpump.h index a23921698..f887e7608 100644 --- a/src/devices/heatpump.h +++ b/src/devices/heatpump.h @@ -37,7 +37,7 @@ class Heatpump : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/mixing.cpp b/src/devices/mixing.cpp index 81e3f3d53..ee8077761 100644 --- a/src/devices/mixing.cpp +++ b/src/devices/mixing.cpp @@ -60,25 +60,31 @@ void Mixing::add_context_menu() { } // output json to web UI -void Mixing::device_info(JsonArray & root) { +void Mixing::device_info_web(JsonArray & root) { if (type_ == Type::NONE) { return; // don't have any values yet } if (type_ == Type::WWC) { render_value_json(root, "", F("Warm Water Circuit"), hc_, nullptr); + render_value_json(root, "", F("Current warm water temperature"), flowTemp_, F_(degrees), 10); + render_value_json(root, "", F("Current pump status"), pump_, nullptr); + render_value_json(root, "", F("Current temperature status"), status_, nullptr); } else { render_value_json(root, "", F("Heating Circuit"), hc_, nullptr); + render_value_json(root, "", F("Current flow temperature"), flowTemp_, F_(degrees), 10); + render_value_json(root, "", F("Setpoint flow temperature"), flowSetTemp_, F_(degrees)); + render_value_json(root, "", F("Current pump status"), pump_, nullptr, EMS_VALUE_BOOL); + render_value_json(root, "", F("Current valve status"), status_, F_(percent)); } - - render_value_json(root, "", F("Current flow temperature"), flowTemp_, F_(degrees), 10); - render_value_json(root, "", F("Setpoint flow temperature"), flowSetTemp_, F_(degrees)); - render_value_json(root, "", F("Current pump modulation"), pumpMod_, F_(percent)); - render_value_json(root, "", F("Current valve status"), status_, nullptr); } // check to see if values have been updated bool Mixing::updated_values() { + if (changed_) { + changed_ = false; + return true; + } return false; } @@ -96,14 +102,17 @@ void Mixing::show_values(uuid::console::Shell & shell) { if (type_ == Type::WWC) { print_value(shell, 2, F("Warm Water Circuit"), hc_, nullptr); + print_value(shell, 4, F("Current warm water temperature"), flowTemp_, F_(degrees), 10); + print_value(shell, 4, F("Current pump status"), pump_, nullptr); + print_value(shell, 4, F("Current temperature status"), status_, nullptr); } else { print_value(shell, 2, F("Heating Circuit"), hc_, nullptr); + print_value(shell, 4, F("Current flow temperature"), flowTemp_, F_(degrees), 10); + print_value(shell, 4, F("Setpoint flow temperature"), flowSetTemp_, F_(degrees)); + print_value(shell, 4, F("Current pump status"), pump_, nullptr, EMS_VALUE_BOOL); + print_value(shell, 4, F("Current valve status"), status_, F_(percent)); } - print_value(shell, 4, F("Current flow temperature"), flowTemp_, F_(degrees), 10); - print_value(shell, 4, F("Setpoint flow temperature"), flowSetTemp_, F_(degrees)); - print_value(shell, 4, F("Current pump modulation"), pumpMod_, F_(percent)); - print_value(shell, 4, F("Current valve status"), status_, nullptr); shell.println(); } @@ -112,37 +121,42 @@ void Mixing::show_values(uuid::console::Shell & shell) { // ideally we should group up all the mixing units together into a nested JSON but for now we'll send them individually void Mixing::publish_values() { StaticJsonDocument doc; + char s[5]; // for formatting strings switch (type_) { case Type::HC: doc["type"] = "hc"; + if (Helpers::hasValue(flowTemp_)) { + doc["flowTemp"] = (float)flowTemp_ / 10; + } + if (Helpers::hasValue(flowSetTemp_)) { + doc["flowSetTemp"] = flowSetTemp_; + } + if (Helpers::hasValue(pump_)) { + doc["pumpStatus"] = Helpers::render_value(s, pump_, EMS_VALUE_BOOL); + } + if (Helpers::hasValue(status_)) { + doc["valveStatus"] = status_; + } break; case Type::WWC: doc["type"] = "wwc"; + if (Helpers::hasValue(flowTemp_)) { + doc["wwTemp"] = (float)flowTemp_ / 10; + } + if (Helpers::hasValue(pump_)) { + doc["pumpStatus"] = pump_; + } + if (Helpers::hasValue(status_)) { + doc["tempStatus"] = status_; + } break; case Type::NONE: default: return; } - if (Helpers::hasValue(flowTemp_)) { - doc["flowTemp"] = (float)flowTemp_ / 10; - } - - if (Helpers::hasValue(pumpMod_)) { - doc["pumpMod"] = pumpMod_; - } - - if (Helpers::hasValue(status_)) { - doc["status"] = status_; - } - - if (Helpers::hasValue(flowSetTemp_)) { - doc["flowSetTemp"] = flowSetTemp_; - } - char topic[30]; - char s[3]; // for formatting strings strlcpy(topic, "mixing_data", 30); strlcat(topic, Helpers::itoa(s, get_device_id() - 0x20 + 1), 30); // append hc to topic Mqtt::publish(topic, doc); @@ -153,11 +167,11 @@ void Mixing::publish_values() { // A0 0B FF 00 01 D7 00 00 00 80 00 00 00 00 03 80 void Mixing::process_MMPLUSStatusMessage_HC(std::shared_ptr telegram) { type_ = Type::HC; - hc_ = telegram->type_id - 0x02D7 + 1; // determine which circuit this is - telegram->read_value(flowTemp_, 3); // is * 10 - telegram->read_value(flowSetTemp_, 5); - telegram->read_value(pumpMod_, 2); - telegram->read_value(status_, 1); // valve status + hc_ = telegram->type_id - 0x02D7 + 1; // determine which circuit this is + changed_ |= telegram->read_value(flowTemp_, 3); // is * 10 + changed_ |= telegram->read_value(flowSetTemp_, 5); + changed_ |= telegram->read_value(pump_, 0); + changed_ |= telegram->read_value(status_, 2); // valve status } // Mixing module warm water loading/DHW - 0x0331, 0x0332 @@ -165,10 +179,10 @@ void Mixing::process_MMPLUSStatusMessage_HC(std::shared_ptr tele // A8 00 FF 00 02 31 02 35 00 3C 00 3C 3C 46 02 03 03 00 3C // in 0x29 void Mixing::process_MMPLUSStatusMessage_WWC(std::shared_ptr telegram) { type_ = Type::WWC; - hc_ = telegram->type_id - 0x0331 + 1; // determine which circuit this is. There are max 2. - telegram->read_value(flowTemp_, 0); // is * 10 - telegram->read_value(pumpMod_, 2); - telegram->read_value(status_, 11); // temp status + hc_ = telegram->type_id - 0x0331 + 1; // determine which circuit this is. There are max 2. + changed_ |= telegram->read_value(flowTemp_, 0); // is * 10 + changed_ |= telegram->read_value(pump_, 2); + changed_ |= telegram->read_value(status_, 11); // temp status } // Mixing IMP - 0x010C @@ -178,20 +192,16 @@ void Mixing::process_IPMStatusMessage(std::shared_ptr telegram) type_ = Type::HC; hc_ = get_device_id() - 0x20 + 1; uint8_t ismixed = 0; - telegram->read_value(ismixed, 0); // check if circuit is active, 0-off, 1-unmixed, 2-mixed + changed_ |= telegram->read_value(ismixed, 0); // check if circuit is active, 0-off, 1-unmixed, 2-mixed if (ismixed == 0) { return; } - if (ismixed == 2) { // we have a mixed circuit - telegram->read_value(flowTemp_, 3); // is * 10 - telegram->read_value(flowSetTemp_, 5); - telegram->read_value(status_, 2); // valve status - } - uint8_t pump = 0xFF; - telegram->read_bitvalue(pump, 1, 0); // pump is also in unmixed circuits - if (pump != 0xFF) { - pumpMod_ = 100 * pump; + if (ismixed == 2) { // we have a mixed circuit + changed_ |= telegram->read_value(flowTemp_, 3); // is * 10 + changed_ |= telegram->read_value(flowSetTemp_, 5); + changed_ |= telegram->read_value(status_, 2); // valve status } + changed_ |= telegram->read_bitvalue(pump_, 1, 0); // pump is also in unmixed circuits } // Mixing on a MM10 - 0xAB @@ -204,9 +214,10 @@ void Mixing::process_MMStatusMessage(std::shared_ptr telegram) { // 0x21 is position 2. 0x20 is typically reserved for the WM10 switch module // see https://github.com/proddy/EMS-ESP/issues/270 and https://github.com/proddy/EMS-ESP/issues/386#issuecomment-629610918 hc_ = get_device_id() - 0x20 + 1; - telegram->read_value(flowTemp_, 1); // is * 10 - telegram->read_value(pumpMod_, 3); - telegram->read_value(flowSetTemp_, 0); + changed_ |= telegram->read_value(flowTemp_, 1); // is * 10 + changed_ |= telegram->read_value(pump_, 3); + changed_ |= telegram->read_value(flowSetTemp_, 0); + changed_ |= telegram->read_value(status_, 4); // valve status -100 to 100 } #pragma GCC diagnostic push diff --git a/src/devices/mixing.h b/src/devices/mixing.h index d1c6b09b9..2dac113d2 100644 --- a/src/devices/mixing.h +++ b/src/devices/mixing.h @@ -37,7 +37,7 @@ class Mixing : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); @@ -62,10 +62,11 @@ class Mixing : public EMSdevice { private: uint16_t hc_ = EMS_VALUE_USHORT_NOTSET; uint16_t flowTemp_ = EMS_VALUE_USHORT_NOTSET; - uint8_t pumpMod_ = EMS_VALUE_UINT_NOTSET; - uint8_t status_ = EMS_VALUE_UINT_NOTSET; + uint8_t pump_ = EMS_VALUE_UINT_NOTSET; + int8_t status_ = EMS_VALUE_UINT_NOTSET; uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET; Type type_ = Type::NONE; + bool changed_ = false; }; } // namespace emsesp diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index c6527e485..f563b02c3 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -54,7 +54,7 @@ void Solar::add_context_menu() { } // print to web -void Solar::device_info(JsonArray & root) { +void Solar::device_info_web(JsonArray & root) { render_value_json(root, "", F("Collector temperature (TS1)"), collectorTemp_, F_(degrees), 10); render_value_json(root, "", F("Tank bottom temperature (TS2)"), tankBottomTemp_, F_(degrees), 10); render_value_json(root, "", F("Tank bottom temperature (TS5)"), tankBottomTemp2_, F_(degrees), 10); @@ -145,7 +145,7 @@ void Solar::publish_values() { } if (Helpers::hasValue(pumpWorkMin_)) { - doc["pumpWorkMin"] = (float)pumpWorkMin_; + doc["pumpWorkMin"] = pumpWorkMin_; } if (Helpers::hasValue(tankHeated_, EMS_VALUE_BOOL)) { @@ -168,11 +168,18 @@ void Solar::publish_values() { doc["energyTotal"] = (float)energyTotal_ / 10; } - Mqtt::publish("sm_data", doc); + // if we have data, publish it + if (!doc.isNull()) { + Mqtt::publish(F("sm_data"), doc); + } } // check to see if values have been updated bool Solar::updated_values() { + if (changed_) { + changed_ = false; + return true; + } return false; } @@ -182,11 +189,11 @@ void Solar::console_commands() { // SM10Monitor - type 0x97 void Solar::process_SM10Monitor(std::shared_ptr telegram) { - telegram->read_value(collectorTemp_, 2); // collector temp from SM10, is *10 - telegram->read_value(tankBottomTemp_, 5); // bottom temp from SM10, is *10 - telegram->read_value(solarPumpModulation_, 4); // modulation solar pump - telegram->read_bitvalue(solarPump_, 7, 1); - telegram->read_value(pumpWorkMin_, 8); + changed_ |= telegram->read_value(collectorTemp_, 2); // collector temp from SM10, is *10 + changed_ |= telegram->read_value(tankBottomTemp_, 5); // bottom temp from SM10, is *10 + changed_ |= telegram->read_value(solarPumpModulation_, 4); // modulation solar pump + changed_ |= telegram->read_bitvalue(solarPump_, 7, 1); + changed_ |= telegram->read_value(pumpWorkMin_, 8); } /* @@ -201,10 +208,10 @@ void Solar::process_SM10Monitor(std::shared_ptr telegram) { * bytes 20+21 = TS6 Temperature sensor external heat exchanger */ void Solar::process_SM100Monitor(std::shared_ptr telegram) { - telegram->read_value(collectorTemp_, 0); // is *10 - TS1: Temperature sensor for collector array 1 - telegram->read_value(tankBottomTemp_, 2); // is *10 - TS2: Temperature sensor 1 cylinder, bottom - telegram->read_value(tankBottomTemp2_, 16); // is *10 - TS5: Temperature sensor 2 cylinder, bottom, or swimming pool - telegram->read_value(heatExchangerTemp_, 20); // is *10 - TS6: Heat exchanger temperature sensor + changed_ |= telegram->read_value(collectorTemp_, 0); // is *10 - TS1: Temperature sensor for collector array 1 + changed_ |= telegram->read_value(tankBottomTemp_, 2); // is *10 - TS2: Temperature sensor 1 cylinder, bottom + changed_ |= telegram->read_value(tankBottomTemp2_, 16); // is *10 - TS5: Temperature sensor 2 cylinder, bottom, or swimming pool + changed_ |= telegram->read_value(heatExchangerTemp_, 20); // is *10 - TS6: Heat exchanger temperature sensor } #pragma GCC diagnostic push @@ -221,9 +228,9 @@ void Solar::process_SM100Monitor2(std::shared_ptr telegram) { // SM100Config - 0x0366 // e.g. B0 00 FF 00 02 66 01 62 00 13 40 14 void Solar::process_SM100Config(std::shared_ptr telegram) { - telegram->read_value(availabilityFlag_, 0); - telegram->read_value(configFlag_, 1); - telegram->read_value(userFlag_, 2); + changed_ |= telegram->read_value(availabilityFlag_, 0); + changed_ |= telegram->read_value(configFlag_, 1); + changed_ |= telegram->read_value(userFlag_, 2); } /* @@ -235,8 +242,8 @@ void Solar::process_SM100Config(std::shared_ptr telegram) { void Solar::process_SM100Status(std::shared_ptr telegram) { uint8_t solarpumpmod = solarPumpModulation_; uint8_t cylinderpumpmod = cylinderPumpModulation_; - telegram->read_value(cylinderPumpModulation_, 8); - telegram->read_value(solarPumpModulation_, 9); + changed_ |= telegram->read_value(cylinderPumpModulation_, 8); + changed_ |= telegram->read_value(solarPumpModulation_, 9); if (solarpumpmod == 0 && solarPumpModulation_ == 100) { // mask out boosts solarPumpModulation_ = 15; // set to minimum @@ -245,8 +252,8 @@ void Solar::process_SM100Status(std::shared_ptr telegram) { if (cylinderpumpmod == 0 && cylinderPumpModulation_ == 100) { // mask out boosts cylinderPumpModulation_ = 15; // set to minimum } - telegram->read_bitvalue(tankHeated_, 3, 1); // issue #422 - telegram->read_bitvalue(collectorShutdown_, 3, 0); // collector shutdown + changed_ |= telegram->read_bitvalue(tankHeated_, 3, 1); // issue #422 + changed_ |= telegram->read_bitvalue(collectorShutdown_, 3, 0); // collector shutdown } /* @@ -256,8 +263,8 @@ void Solar::process_SM100Status(std::shared_ptr telegram) { * byte 10 = PS1 Solar circuit pump for collector array 1: test=b0001(1), on=b0100(4) and off=b0011(3) */ void Solar::process_SM100Status2(std::shared_ptr telegram) { - telegram->read_bitvalue(valveStatus_, 4, 2); // on if bit 2 set - telegram->read_bitvalue(solarPump_, 10, 2); // on if bit 2 set + changed_ |= telegram->read_bitvalue(valveStatus_, 4, 2); // on if bit 2 set + changed_ |= telegram->read_bitvalue(solarPump_, 10, 2); // on if bit 2 set } /* @@ -265,9 +272,9 @@ void Solar::process_SM100Status2(std::shared_ptr telegram) { * e.g. 30 00 FF 00 02 8E 00 00 00 00 00 00 06 C5 00 00 76 35 */ void Solar::process_SM100Energy(std::shared_ptr telegram) { - telegram->read_value(energyLastHour_, 0); // last hour / 10 in Wh - telegram->read_value(energyToday_, 4); // todays in Wh - telegram->read_value(energyTotal_, 8); // total / 10 in kWh + changed_ |= telegram->read_value(energyLastHour_, 0); // last hour / 10 in Wh + changed_ |= telegram->read_value(energyToday_, 4); // todays in Wh + changed_ |= telegram->read_value(energyTotal_, 8); // total / 10 in kWh } /* @@ -275,26 +282,26 @@ void Solar::process_SM100Energy(std::shared_ptr telegram) { * e.g. B0 00 FF 00 00 03 32 00 00 00 00 13 00 D6 00 00 00 FB D0 F0 */ void Solar::process_ISM1StatusMessage(std::shared_ptr telegram) { - telegram->read_value(collectorTemp_, 4); // Collector Temperature - telegram->read_value(tankBottomTemp_, 6); // Temperature Bottom of Solar Boiler + changed_ |= telegram->read_value(collectorTemp_, 4); // Collector Temperature + changed_ |= telegram->read_value(tankBottomTemp_, 6); // Temperature Bottom of Solar Boiler uint16_t Wh = 0xFFFF; - telegram->read_value(Wh, 2); // Solar Energy produced in last hour only ushort, is not * 10 + changed_ |= telegram->read_value(Wh, 2); // Solar Energy produced in last hour only ushort, is not * 10 if (Wh != 0xFFFF) { energyLastHour_ = Wh * 10; // set to *10 } - telegram->read_bitvalue(solarPump_, 8, 0); // PS1 Solar pump on (1) or off (0) - telegram->read_value(pumpWorkMin_, 10, 3); // force to 3 bytes - telegram->read_bitvalue(collectorShutdown_, 9, 0); // collector shutdown on/off - telegram->read_bitvalue(tankHeated_, 9, 2); // tank full + changed_ |= telegram->read_bitvalue(solarPump_, 8, 0); // PS1 Solar pump on (1) or off (0) + changed_ |= telegram->read_value(pumpWorkMin_, 10, 3); // force to 3 bytes + changed_ |= telegram->read_bitvalue(collectorShutdown_, 9, 0); // collector shutdown on/off + changed_ |= telegram->read_bitvalue(tankHeated_, 9, 2); // tank full } /* * Junkers ISM1 Solar Module - type 0x0101 EMS+ for setting values */ void Solar::process_ISM1Set(std::shared_ptr telegram) { - telegram->read_value(setpoint_maxBottomTemp_, 6); + changed_ |= telegram->read_value(setpoint_maxBottomTemp_, 6); } } // namespace emsesp diff --git a/src/devices/solar.h b/src/devices/solar.h index 4c85cd1fb..248cd71ec 100644 --- a/src/devices/solar.h +++ b/src/devices/solar.h @@ -37,7 +37,7 @@ class Solar : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); @@ -65,6 +65,7 @@ class Solar : public EMSdevice { uint8_t availabilityFlag_ = EMS_VALUE_BOOL_NOTSET; uint8_t configFlag_ = EMS_VALUE_BOOL_NOTSET; uint8_t userFlag_ = EMS_VALUE_BOOL_NOTSET; + bool changed_ = false; void process_SM10Monitor(std::shared_ptr telegram); void process_SM100Monitor(std::shared_ptr telegram); diff --git a/src/devices/switch.cpp b/src/devices/switch.cpp index 95d1f4454..61a7c56e7 100644 --- a/src/devices/switch.cpp +++ b/src/devices/switch.cpp @@ -31,7 +31,7 @@ Switch::Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const void Switch::add_context_menu() { } -void Switch::device_info(JsonArray & root) { +void Switch::device_info_web(JsonArray & root) { } // display all values into the shell console diff --git a/src/devices/switch.h b/src/devices/switch.h index 44d9844d6..083664cd6 100644 --- a/src/devices/switch.h +++ b/src/devices/switch.h @@ -37,7 +37,7 @@ class Switch : public EMSdevice { virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index c5783afd2..d135f7b4a 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -26,7 +26,22 @@ uuid::log::Logger Thermostat::logger_{F_(thermostat), uuid::log::Facility::CONSO Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { - if (EMSESP::actual_master_thermostat() == 0) { + uint8_t actual_master_thermostat = EMSESP::actual_master_thermostat(); // what we're actually using + uint8_t master_thermostat = EMSESP_DEFAULT_MASTER_THERMOSTAT; + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { + master_thermostat = settings.master_thermostat; // what the user has defined + }); + + uint8_t model = this->model(); + + // if we're on auto mode, register this thermostat if it has a device id of 0x10, 0x17 or 0x18 + // or if its the master thermostat we defined + // see https://github.com/proddy/EMS-ESP/issues/362#issuecomment-629628161 + if ((master_thermostat == device_id) + || ((master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) && (device_id < 0x19) + && ((actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) || (device_id < actual_master_thermostat)))) { + EMSESP::actual_master_thermostat(device_id); + actual_master_thermostat = device_id; this->reserve_mem(25); // reserve some space for the telegram registries, to avoid memory fragmentation // common telegram handlers @@ -34,7 +49,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(EMS_TYPE_RCTime, F("RCTime"), false, [&](std::shared_ptr t) { process_RCTime(t); }); } // RC10 - if (flags == EMSdevice::EMS_DEVICE_FLAG_RC10) { + if (model == EMSdevice::EMS_DEVICE_FLAG_RC10) { monitor_typeids = {0xB1}; set_typeids = {0xB0}; for (uint8_t i = 0; i < monitor_typeids.size(); i++) { @@ -43,7 +58,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i } // RC35 - } else if ((flags == EMSdevice::EMS_DEVICE_FLAG_RC35) || (flags == EMSdevice::EMS_DEVICE_FLAG_RC30_1)) { + } else if ((model == EMSdevice::EMS_DEVICE_FLAG_RC35) || (model == EMSdevice::EMS_DEVICE_FLAG_RC30_1)) { monitor_typeids = {0x3E, 0x48, 0x52, 0x5C}; set_typeids = {0x3D, 0x47, 0x51, 0x5B}; timer_typeids = {0x3F, 0x49, 0x53, 0x5D}; @@ -55,10 +70,10 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(EMS_TYPE_wwSettings, F("WWSettings"), true, [&](std::shared_ptr t) { process_RC35wwSettings(t); }); // RC20 - } else if (flags == EMSdevice::EMS_DEVICE_FLAG_RC20) { + } else if (model == EMSdevice::EMS_DEVICE_FLAG_RC20) { monitor_typeids = {0x91}; set_typeids = {0xA8}; - if (EMSESP::actual_master_thermostat() == 0) { + if (actual_master_thermostat == device_id) { for (uint8_t i = 0; i < monitor_typeids.size(); i++) { register_telegram_type(monitor_typeids[i], F("RC20Monitor"), false, [&](std::shared_ptr t) { process_RC20Monitor(t); }); register_telegram_type(set_typeids[i], F("RC20Set"), false, [&](std::shared_ptr t) { process_RC20Set(t); }); @@ -67,10 +82,10 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(0xAF, F("RC20Remote"), false, [&](std::shared_ptr t) { process_RC20Remote(t); }); } // RC20 newer - } else if (flags == EMSdevice::EMS_DEVICE_FLAG_RC20_2) { + } else if (model == EMSdevice::EMS_DEVICE_FLAG_RC20_2) { monitor_typeids = {0xAE}; set_typeids = {0xAD}; - if (EMSESP::actual_master_thermostat() == 0) { + if (actual_master_thermostat == device_id) { for (uint8_t i = 0; i < monitor_typeids.size(); i++) { register_telegram_type(monitor_typeids[i], F("RC20Monitor"), false, [&](std::shared_ptr t) { process_RC20Monitor_2(t); }); register_telegram_type(set_typeids[i], F("RC20Set"), false, [&](std::shared_ptr t) { process_RC20Set_2(t); }); @@ -79,7 +94,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(0xAF, F("RC20Remote"), false, [&](std::shared_ptr t) { process_RC20Remote(t); }); } // RC30 - } else if (flags == EMSdevice::EMS_DEVICE_FLAG_RC30) { + } else if (model == EMSdevice::EMS_DEVICE_FLAG_RC30) { monitor_typeids = {0x41}; set_typeids = {0xA7}; for (uint8_t i = 0; i < monitor_typeids.size(); i++) { @@ -88,13 +103,13 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i } // EASY - } else if (flags == EMSdevice::EMS_DEVICE_FLAG_EASY) { + } else if (model == EMSdevice::EMS_DEVICE_FLAG_EASY) { monitor_typeids = {0x0A}; set_typeids = {}; - register_telegram_type(monitor_typeids[0], F("EasyMonitor"), false, [&](std::shared_ptr t) { process_EasyMonitor(t); }); + register_telegram_type(monitor_typeids[0], F("EasyMonitor"), true, [&](std::shared_ptr t) { process_EasyMonitor(t); }); // RC300/RC100 - } else if ((flags == EMSdevice::EMS_DEVICE_FLAG_RC300) || (flags == EMSdevice::EMS_DEVICE_FLAG_RC100)) { + } else if ((model == EMSdevice::EMS_DEVICE_FLAG_RC300) || (model == EMSdevice::EMS_DEVICE_FLAG_RC100)) { monitor_typeids = {0x02A5, 0x02A6, 0x02A7, 0x02A8}; set_typeids = {0x02B9, 0x02BA, 0x02BB, 0x02BC}; for (uint8_t i = 0; i < monitor_typeids.size(); i++) { @@ -105,7 +120,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(0x31E, F("RC300WWmode"), false, [&](std::shared_ptr t) { process_RC300WWmode(t); }); // JUNKERS/HT3 - } else if (flags == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { + } else if (model == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { monitor_typeids = {0x016F, 0x0170, 0x0171, 0x0172}; set_typeids = {0x0165, 0x0166, 0x0167, 0x0168}; for (uint8_t i = 0; i < monitor_typeids.size(); i++) { @@ -113,7 +128,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(set_typeids[i], F("JunkersSet"), false, [&](std::shared_ptr t) { process_JunkersSet(t); }); } - } else if (flags == (EMSdevice::EMS_DEVICE_FLAG_JUNKERS | EMSdevice::EMS_DEVICE_FLAG_JUNKERS_2)) { + } else if (model == (EMSdevice::EMS_DEVICE_FLAG_JUNKERS | EMSdevice::EMS_DEVICE_FLAG_JUNKERS_2)) { monitor_typeids = {0x016F, 0x0170, 0x0171, 0x0172}; set_typeids = {0x0179, 0x017A, 0x017B, 0x017C}; for (uint8_t i = 0; i < monitor_typeids.size(); i++) { @@ -122,29 +137,16 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i } } - uint8_t master_thermostat = 0; - EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { - master_thermostat = settings.master_thermostat; // what the user has defined - }); - EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { mqtt_format_ = settings.mqtt_format; // single, nested or ha }); - uint8_t actual_master_thermostat = EMSESP::actual_master_thermostat(); // what we're actually using - uint8_t num_devices = EMSESP::count_devices(EMSdevice::DeviceType::THERMOSTAT) + 1; // including this thermostat - - // if we're on auto mode, register this thermostat if it has a device id of 0x10, 0x17 or 0x18 - // or if its the master thermostat we defined - // see https://github.com/proddy/EMS-ESP/issues/362#issuecomment-629628161 - if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT)) || (master_thermostat == device_id)) { - EMSESP::actual_master_thermostat(device_id); - LOG_DEBUG(F("Adding new thermostat with device ID 0x%02X (as master)"), device_id); - add_commands(); - } else { + if (actual_master_thermostat != device_id) { LOG_DEBUG(F("Adding new thermostat with device ID 0x%02X"), device_id); return; // don't fetch data if more than 1 thermostat } + LOG_DEBUG(F("Adding new thermostat with device ID 0x%02X (as master)"), device_id); + add_commands(); // reserve some memory for the heating circuits (max 4 to start with) heating_circuits_.reserve(4); @@ -161,8 +163,8 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i } // prepare data for Web UI -void Thermostat::device_info(JsonArray & root) { - uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, strip the option bits +void Thermostat::device_info_web(JsonArray & root) { + uint8_t flags = this->model(); for (const auto & hc : heating_circuits_) { if (!Helpers::hasValue(hc->setpoint_roomTemp)) { @@ -240,23 +242,10 @@ bool Thermostat::updated_values() { if (EMSESP::actual_master_thermostat() != this->get_device_id()) { return false; } - - // quick hack to see if it changed. We simply just add up all the raw values - uint16_t new_value = 0; - static uint16_t current_value_ = 0; - for (const auto & hc : heating_circuits_) { - // don't publish if we haven't yet received some data - if (!Helpers::hasValue(hc->setpoint_roomTemp)) { - return false; - } - new_value += hc->setpoint_roomTemp + hc->curr_roomTemp + hc->mode; - } - - if (new_value != current_value_) { - current_value_ = new_value; + if (changed_) { + changed_ = false; return true; } - return false; } @@ -267,15 +256,15 @@ void Thermostat::publish_values() { return; } - uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, stripping the option bits - bool has_data = false; + uint8_t flags = this->model(); StaticJsonDocument doc; JsonObject rootThermostat = doc.to(); JsonObject dataThermostat; // add external temp and other stuff specific to the RC30 and RC35 - if ((flags == EMS_DEVICE_FLAG_RC35 || flags == EMS_DEVICE_FLAG_RC30_1) && (mqtt_format_ == MQTT_format::SINGLE || mqtt_format_ == MQTT_format::CUSTOM)) { + // if ((flags == EMS_DEVICE_FLAG_RC35 || flags == EMS_DEVICE_FLAG_RC30_1) && (mqtt_format_ == MQTT_format::SINGLE || mqtt_format_ == MQTT_format::CUSTOM)) { + if (flags == EMS_DEVICE_FLAG_RC35 || flags == EMS_DEVICE_FLAG_RC30_1) { if (datetime_.size()) { rootThermostat["time"] = datetime_.c_str(); } @@ -314,135 +303,140 @@ void Thermostat::publish_values() { } // send this specific data using the thermostat_data topic - // if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::HA)) { if (mqtt_format_ != MQTT_format::NESTED) { - Mqtt::publish("thermostat_data", doc); + Mqtt::publish(F("thermostat_data"), doc); rootThermostat = doc.to(); // clear object } } // go through all the heating circuits + bool has_data = false; for (const auto & hc : heating_circuits_) { - if (!Helpers::hasValue(hc->setpoint_roomTemp)) { - break; // skip this HC - } + if (hc->is_active()) { + has_data = true; - has_data = true; - // if the MQTT format is 'nested' or 'ha' then create the parent object hc - // if (mqtt_format_ != MQTT_format::SINGLE) { - if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::HA)) { - char hc_name[10]; // hc{1-4} - strlcpy(hc_name, "hc", 10); - char s[3]; - strlcat(hc_name, Helpers::itoa(s, hc->hc_num()), 10); - dataThermostat = rootThermostat.createNestedObject(hc_name); - } else { - dataThermostat = rootThermostat; - } - - // different logic on how temperature values are stored, depending on model - uint8_t setpoint_temp_divider; - uint8_t curr_temp_divider; - if (flags == EMS_DEVICE_FLAG_EASY) { - setpoint_temp_divider = 100; - curr_temp_divider = 100; - } else if (flags == EMS_DEVICE_FLAG_JUNKERS) { - setpoint_temp_divider = 10; - curr_temp_divider = 10; - } else { - setpoint_temp_divider = 2; - curr_temp_divider = 10; - } - - if (Helpers::hasValue(hc->setpoint_roomTemp)) { - dataThermostat["seltemp"] = Helpers::round2((float)hc->setpoint_roomTemp / setpoint_temp_divider); - } - - if (Helpers::hasValue(hc->curr_roomTemp)) { - dataThermostat["currtemp"] = Helpers::round2((float)hc->curr_roomTemp / curr_temp_divider); - } - - if (Helpers::hasValue(hc->daytemp)) { - if (flags == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { - dataThermostat["heattemp"] = (float)hc->daytemp / 2; + // if the MQTT format is 'nested' or 'ha' then create the parent object hc + // if (mqtt_format_ != MQTT_format::SINGLE) { + if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::HA)) { + char hc_name[10]; // hc{1-4} + strlcpy(hc_name, "hc", 10); + char s[3]; + strlcat(hc_name, Helpers::itoa(s, hc->hc_num()), 10); + dataThermostat = rootThermostat.createNestedObject(hc_name); } else { - dataThermostat["daytemp"] = (float)hc->daytemp / 2; + dataThermostat = rootThermostat; } - } - if (Helpers::hasValue(hc->nighttemp)) { - if (flags == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { - dataThermostat["ecotemp"] = (float)hc->nighttemp / 2; + + // different logic on how temperature values are stored, depending on model + uint8_t setpoint_temp_divider; + uint8_t curr_temp_divider; + if (flags == EMS_DEVICE_FLAG_EASY) { + setpoint_temp_divider = 100; + curr_temp_divider = 100; + } else if (flags == EMS_DEVICE_FLAG_JUNKERS) { + setpoint_temp_divider = 10; + curr_temp_divider = 10; } else { - dataThermostat["nighttemp"] = (float)hc->nighttemp / 2; + setpoint_temp_divider = 2; + curr_temp_divider = 10; } - } - if (Helpers::hasValue(hc->holidaytemp)) { - dataThermostat["holidaytemp"] = (float)hc->holidaytemp / 2; - } - if (Helpers::hasValue(hc->nofrosttemp)) { - dataThermostat["nofrosttemp"] = (float)hc->nofrosttemp / 2; - } + if (Helpers::hasValue(hc->setpoint_roomTemp)) { + dataThermostat["seltemp"] = Helpers::round2((float)hc->setpoint_roomTemp / setpoint_temp_divider); + } - if (Helpers::hasValue(hc->heatingtype)) { - dataThermostat["heatingtype"] = hc->heatingtype; - } + if (Helpers::hasValue(hc->curr_roomTemp)) { + dataThermostat["currtemp"] = Helpers::round2((float)hc->curr_roomTemp / curr_temp_divider); + } - if (Helpers::hasValue(hc->targetflowtemp)) { - dataThermostat["targetflowtemp"] = hc->targetflowtemp; - } - - if (Helpers::hasValue(hc->offsettemp)) { - dataThermostat["offsettemp"] = hc->offsettemp / 2; - } - - if (Helpers::hasValue(hc->designtemp)) { - dataThermostat["designtemp"] = hc->designtemp; - } - if (Helpers::hasValue(hc->summertemp)) { - dataThermostat["summertemp"] = hc->summertemp; - } - - // when using HA always send the mode otherwise it'll may break the component/widget and report an error - if ((Helpers::hasValue(hc->mode)) || (mqtt_format_ == MQTT_format::HA)) { - uint8_t hc_mode = hc->get_mode(flags); - // if we're sending to HA the only valid mode types are heat, auto and off - if (mqtt_format_ == MQTT_format::HA) { - if ((hc_mode == HeatingCircuit::Mode::MANUAL) || (hc_mode == HeatingCircuit::Mode::DAY)) { - hc_mode = HeatingCircuit::Mode::HEAT; - } else if ((hc_mode == HeatingCircuit::Mode::NIGHT) || (hc_mode == HeatingCircuit::Mode::OFF)) { - hc_mode = HeatingCircuit::Mode::OFF; + if (Helpers::hasValue(hc->daytemp)) { + if (flags == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { + dataThermostat["heattemp"] = (float)hc->daytemp / 2; } else { - hc_mode = HeatingCircuit::Mode::AUTO; + dataThermostat["daytemp"] = (float)hc->daytemp / 2; } } - dataThermostat["mode"] = mode_tostring(hc_mode); - } + if (Helpers::hasValue(hc->nighttemp)) { + if (flags == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { + dataThermostat["ecotemp"] = (float)hc->nighttemp / 2; + } else { + dataThermostat["nighttemp"] = (float)hc->nighttemp / 2; + } + } + if (Helpers::hasValue(hc->holidaytemp)) { + dataThermostat["holidaytemp"] = (float)hc->holidaytemp / 2; + } - // special handling of mode type, for the RC35 replace with summer/holiday if set - // https://github.com/proddy/EMS-ESP/issues/373#issuecomment-619810209 - if (Helpers::hasValue(hc->summer_mode) && hc->summer_mode) { - dataThermostat["modetype"] = F("summer"); - } else if (Helpers::hasValue(hc->holiday_mode) && hc->holiday_mode) { - dataThermostat["modetype"] = F("holiday"); - } else if (Helpers::hasValue(hc->mode_type)) { - dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags)); - } + if (Helpers::hasValue(hc->nofrosttemp)) { + dataThermostat["nofrosttemp"] = (float)hc->nofrosttemp / 2; + } - // if format is single, send immediately and clear object for next hc - // the topic will have the hc number appended - // if (mqtt_format_ == MQTT_format::SINGLE) { - if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::CUSTOM)) { - char topic[30]; - char s[3]; - strlcpy(topic, "thermostat_data", 30); - strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic - Mqtt::publish(topic, doc); - rootThermostat = doc.to(); // clear object - } else if (mqtt_format_ == MQTT_format::HA) { - std::string topic(100, '\0'); - snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/state"), hc->hc_num()); - Mqtt::publish(topic, doc); + if (Helpers::hasValue(hc->heatingtype)) { + dataThermostat["heatingtype"] = hc->heatingtype; + } + + if (Helpers::hasValue(hc->targetflowtemp)) { + dataThermostat["targetflowtemp"] = hc->targetflowtemp; + } + + if (Helpers::hasValue(hc->offsettemp)) { + dataThermostat["offsettemp"] = hc->offsettemp / 2; + } + + if (Helpers::hasValue(hc->designtemp)) { + dataThermostat["designtemp"] = hc->designtemp; + } + if (Helpers::hasValue(hc->summertemp)) { + dataThermostat["summertemp"] = hc->summertemp; + } + + // when using HA always send the mode otherwise it'll may break the component/widget and report an error + if ((Helpers::hasValue(hc->mode)) || (mqtt_format_ == MQTT_format::HA)) { + uint8_t hc_mode = hc->get_mode(flags); + // if we're sending to HA the only valid mode types are heat, auto and off + if (mqtt_format_ == MQTT_format::HA) { + if ((hc_mode == HeatingCircuit::Mode::MANUAL) || (hc_mode == HeatingCircuit::Mode::DAY)) { + hc_mode = HeatingCircuit::Mode::HEAT; + } else if ((hc_mode == HeatingCircuit::Mode::NIGHT) || (hc_mode == HeatingCircuit::Mode::OFF)) { + hc_mode = HeatingCircuit::Mode::OFF; + } else { + hc_mode = HeatingCircuit::Mode::AUTO; + } + } + dataThermostat["mode"] = mode_tostring(hc_mode); + } + + // special handling of mode type, for the RC35 replace with summer/holiday if set + // https://github.com/proddy/EMS-ESP/issues/373#issuecomment-619810209 + if (Helpers::hasValue(hc->summer_mode) && hc->summer_mode) { + dataThermostat["modetype"] = F("summer"); + } else if (Helpers::hasValue(hc->holiday_mode) && hc->holiday_mode) { + dataThermostat["modetype"] = F("holiday"); + } else if (Helpers::hasValue(hc->mode_type)) { + dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags)); + } + + // if format is single, send immediately and clear object for next hc + // the topic will have the hc number appended + // if (mqtt_format_ == MQTT_format::SINGLE) { + if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::CUSTOM)) { + char topic[30]; + char s[3]; + strlcpy(topic, "thermostat_data", 30); + strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic + Mqtt::publish(topic, doc); + rootThermostat = doc.to(); // clear object + } else if (mqtt_format_ == MQTT_format::HA) { + // see if we have already registered this with HA MQTT Discovery, if not send the config + if (!hc->ha_registered()) { + register_mqtt_ha_config(hc->hc_num()); + hc->ha_registered(true); + } + // send the thermostat topic and payload data + std::string topic(100, '\0'); + snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/state"), hc->hc_num()); + Mqtt::publish(topic, doc); + } } } @@ -451,9 +445,8 @@ void Thermostat::publish_values() { } // if we're using nested json, send all in one go under one topic called thermostat_data - // if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::CUSTOM)) { if (mqtt_format_ == MQTT_format::NESTED) { - Mqtt::publish("thermostat_data", doc); + Mqtt::publish(F("thermostat_data"), doc); } } @@ -461,7 +454,7 @@ void Thermostat::publish_values() { // of nullptr if it doesn't exist yet std::shared_ptr Thermostat::heating_circuit(const uint8_t hc_num) { // if hc_num is 0 then return the first existing hc in the list - if (hc_num == 0) { + if (hc_num == AUTO_HEATING_CIRCUIT) { for (const auto & heating_circuit : heating_circuits_) { return heating_circuit; } @@ -522,19 +515,17 @@ std::shared_ptr Thermostat::heating_circuit(std::sha } // create a new heating circuit object - auto new_hc = std::make_shared(hc_num, monitor_typeids[hc_num - 1], set_typeids[hc_num - 1]); + auto new_hc = std::make_shared(hc_num); heating_circuits_.push_back(new_hc); std::sort(heating_circuits_.begin(), heating_circuits_.end()); // sort based on hc number - // if we're using Home Assistant and HA discovery, register the new config - if (mqtt_format_ == MQTT_format::HA) { - register_mqtt_ha_config(hc_num); - } - // set the flag saying we want its data during the next auto fetch toggle_fetch(monitor_typeids[hc_num - 1], toggle_); - toggle_fetch(set_typeids[hc_num - 1], toggle_); + + if (set_typeids.size()) { + toggle_fetch(set_typeids[hc_num - 1], toggle_); + } return heating_circuits_.back(); // even after sorting, this should still point back to the newly created HC } @@ -579,9 +570,24 @@ void Thermostat::register_mqtt_ha_config(uint8_t hc_num) { doc["temp_step"] = "0.5"; JsonArray modes = doc.createNestedArray("modes"); - modes.add("off"); - modes.add("heat"); - modes.add("auto"); + uint8_t flags = this->model(); + if (flags == EMSdevice::EMS_DEVICE_FLAG_RC20_2) { + modes.add("night"); + modes.add("day"); + } else if ((flags == EMSdevice::EMS_DEVICE_FLAG_RC300) || (flags == EMSdevice::EMS_DEVICE_FLAG_RC100)) { + modes.add("eco"); + modes.add("comfort"); + modes.add("auto"); + } else if (flags == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { + modes.add("nofrost"); + modes.add("eco"); + modes.add("heat"); + modes.add("auto"); + } else { // default for all other thermostats + modes.add("night"); + modes.add("day"); + modes.add("auto"); + } std::string topic(100, '\0'); // e.g homeassistant/climate/hc1/thermostat/config snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/config"), hc_num); @@ -725,7 +731,7 @@ std::string Thermostat::mode_tostring(uint8_t mode) { void Thermostat::show_values(uuid::console::Shell & shell) { EMSdevice::show_values(shell); // always call this to show header - uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, strip the option bits + uint8_t flags = this->model(); if (datetime_.size()) { shell.printfln(F(" Clock: %s"), datetime_.c_str()); @@ -804,9 +810,10 @@ void Thermostat::show_values(uuid::console::Shell & shell) { } for (const auto & hc : heating_circuits_) { - if (!Helpers::hasValue(hc->setpoint_roomTemp)) { + if (!hc->is_active()) { break; // skip this HC } + shell.printfln(F(" Heating Circuit %d:"), hc->hc_num()); // different thermostat types store their temperature values differently @@ -882,16 +889,16 @@ void Thermostat::show_values(uuid::console::Shell & shell) { void Thermostat::process_RC20Set(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->mode, 23); + changed_ |= telegram->read_value(hc->mode, 23); } // type 0xAE - data from the RC20 thermostat (0x17) void Thermostat::process_RC20Monitor_2(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_bitvalue(hc->mode_type, 0, 7); // day/night MSB 7th bit is day - telegram->read_value(hc->setpoint_roomTemp, 2, 1); // is * 2, force as single byte - telegram->read_value(hc->curr_roomTemp, 3); // is * 10 + changed_ |= telegram->read_bitvalue(hc->mode_type, 0, 7); // day/night MSB 7th bit is day + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 2, 1); // is * 2, force as single byte + changed_ |= telegram->read_value(hc->curr_roomTemp, 3); // is * 10 } // 0xAD - for reading the mode from the RC20/ES72 thermostat (0x17) @@ -899,21 +906,21 @@ void Thermostat::process_RC20Monitor_2(std::shared_ptr telegram) void Thermostat::process_RC20Set_2(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->mode, 3); + changed_ |= telegram->read_value(hc->mode, 3); } // 0xAF - for reading the roomtemperature from the RC20/ES72 thermostat (0x18, 0x19, ..) void Thermostat::process_RC20Remote(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->curr_roomTemp, 0); + changed_ |= telegram->read_value(hc->curr_roomTemp, 0); } // type 0xB1 - data from the RC10 thermostat (0x17) void Thermostat::process_RC10Monitor(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->setpoint_roomTemp, 1, 1); // is * 2, force as single byte - telegram->read_value(hc->curr_roomTemp, 2); // is * 10 + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 1, 1); // is * 2, force as single byte + changed_ |= telegram->read_value(hc->curr_roomTemp, 2); // is * 10 } #pragma GCC diagnostic push @@ -928,56 +935,57 @@ void Thermostat::process_RC10Set(std::shared_ptr telegram) { // type 0x0165, ff void Thermostat::process_JunkersSet(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->daytemp, 17); // is * 2 - telegram->read_value(hc->nighttemp, 16); // is * 2 - telegram->read_value(hc->nofrosttemp, 15); // is * 2 + changed_ |= telegram->read_value(hc->daytemp, 17); // is * 2 + changed_ |= telegram->read_value(hc->nighttemp, 16); // is * 2 + changed_ |= telegram->read_value(hc->nofrosttemp, 15); // is * 2 } // type 0x0179, ff void Thermostat::process_JunkersSet2(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->daytemp, 7); // is * 2 - telegram->read_value(hc->nighttemp, 6); // is * 2 - telegram->read_value(hc->nofrosttemp, 5); // is * 2 + changed_ |= telegram->read_value(hc->daytemp, 7); // is * 2 + changed_ |= telegram->read_value(hc->nighttemp, 6); // is * 2 + changed_ |= telegram->read_value(hc->nofrosttemp, 5); // is * 2 } // type 0xA3 - for external temp settings from the the RC* thermostats (e.g. RC35) void Thermostat::process_RCOutdoorTemp(std::shared_ptr telegram) { - telegram->read_value(dampedoutdoortemp_, 0); - telegram->read_value(tempsensor1_, 3); // sensor 1 - is * 10 - telegram->read_value(tempsensor2_, 5); // sensor 2 - is * 10 + changed_ |= telegram->read_value(dampedoutdoortemp_, 0); + changed_ |= telegram->read_value(tempsensor1_, 3); // sensor 1 - is * 10 + changed_ |= telegram->read_value(tempsensor2_, 5); // sensor 2 - is * 10 } // 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long void Thermostat::process_RC20Monitor(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->setpoint_roomTemp, 1, 1); // is * 2, force as single byte - telegram->read_value(hc->curr_roomTemp, 2); // is * 10 + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 1, 1); // is * 2, force as single byte + changed_ |= telegram->read_value(hc->curr_roomTemp, 2); // is * 10 } // type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long void Thermostat::process_EasyMonitor(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->curr_roomTemp, 8); // is * 100 - telegram->read_value(hc->setpoint_roomTemp, 10); // is * 100 + changed_ |= telegram->read_value(hc->curr_roomTemp, 8); // is * 100 + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 10); // is * 100 } // Settings Parameters - 0xA5 - RC30_1 void Thermostat::process_IBASettings(std::shared_ptr telegram) { // 22 - display line on RC35 - telegram->read_value(ibaMainDisplay_, - 0); // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 8 smoke temp - telegram->read_value(ibaLanguage_, 1); // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian - telegram->read_value(ibaCalIntTemperature_, 2); // offset int. temperature sensor, by * 0.1 Kelvin - telegram->read_value(ibaBuildingType_, 6); // building type: 0 = light, 1 = medium, 2 = heavy - telegram->read_value(ibaMinExtTemperature_, 5); // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 - telegram->read_value(ibaClockOffset_, 12); // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s + changed_ |= + telegram->read_value(ibaMainDisplay_, + 0); // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 8 smoke temp + changed_ |= telegram->read_value(ibaLanguage_, 1); // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian + changed_ |= telegram->read_value(ibaCalIntTemperature_, 2); // offset int. temperature sensor, by * 0.1 Kelvin + changed_ |= telegram->read_value(ibaBuildingType_, 6); // building type: 0 = light, 1 = medium, 2 = heavy + changed_ |= telegram->read_value(ibaMinExtTemperature_, 5); // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 + changed_ |= telegram->read_value(ibaClockOffset_, 12); // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s } // Settings WW 0x37 - RC35 void Thermostat::process_RC35wwSettings(std::shared_ptr telegram) { - telegram->read_value(wwMode_, 2); // 0 off, 1-on, 2-auto + changed_ |= telegram->read_value(wwMode_, 2); // 0 off, 1-on, 2-auto } // type 0x6F - FR10/FR50/FR100 Junkers @@ -989,21 +997,21 @@ void Thermostat::process_JunkersMonitor(std::shared_ptr telegram std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->curr_roomTemp, 4); // value is * 10 - telegram->read_value(hc->setpoint_roomTemp, 2); // value is * 10 + changed_ |= telegram->read_value(hc->curr_roomTemp, 4); // value is * 10 + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 2); // value is * 10 - telegram->read_value(hc->mode_type, 0); // 1 = nofrost, 2 = eco, 3 = heat - telegram->read_value(hc->mode, 1); // 1 = manual, 2 = auto + changed_ |= telegram->read_value(hc->mode_type, 0); // 1 = nofrost, 2 = eco, 3 = heat + changed_ |= telegram->read_value(hc->mode, 1); // 1 = manual, 2 = auto } // type 0x02A5 - data from the Nefit RC1010/3000 thermostat (0x18) and RC300/310s on 0x10 void Thermostat::process_RC300Monitor(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->curr_roomTemp, 0); // is * 10 + changed_ |= telegram->read_value(hc->curr_roomTemp, 0); // is * 10 - telegram->read_bitvalue(hc->mode_type, 10, 1); - telegram->read_bitvalue(hc->mode, 10, 0); // bit 1, mode (auto=1 or manual=0) + changed_ |= telegram->read_bitvalue(hc->mode_type, 10, 1); + changed_ |= telegram->read_bitvalue(hc->mode, 10, 0); // bit 1, mode (auto=1 or manual=0) // if manual, take the current setpoint temp at pos 6 // if auto, take the next setpoint temp at pos 7 @@ -1012,9 +1020,9 @@ void Thermostat::process_RC300Monitor(std::shared_ptr telegram) // pos 3 actual setpoint (optimized), i.e. changes with temporary change, summer/holiday-modes // pos 6 actual setpoint according to programmed changes eco/comfort // pos 7 next setpoint in the future, time to next setpoint in pos 8/9 - telegram->read_value(hc->setpoint_roomTemp, 3, 1); // is * 2, force as single byte - telegram->read_bitvalue(hc->summer_mode, 2, 4); - telegram->read_value(hc->targetflowtemp, 4); + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 3, 1); // is * 2, force as single byte + changed_ |= telegram->read_bitvalue(hc->summer_mode, 2, 4); + changed_ |= telegram->read_value(hc->targetflowtemp, 4); } // type 0x02B9 EMS+ for reading from RC300/RC310 thermostat @@ -1026,21 +1034,21 @@ void Thermostat::process_RC300Set(std::shared_ptr telegram) { // comfort is position 2 // I think auto is position 8? // actual setpoint taken from RC300Monitor (Michael 12.06.2020) - // telegram->read_value(hc->setpoint_roomTemp, 8, 1); // single byte conversion, value is * 2 - auto? - // telegram->read_value(hc->setpoint_roomTemp, 10, 1); // single byte conversion, value is * 2 - manual + // changed_ |= telegram->read_value(hc->setpoint_roomTemp, 8, 1); // single byte conversion, value is * 2 - auto? + // changed_ |= telegram->read_value(hc->setpoint_roomTemp, 10, 1); // single byte conversion, value is * 2 - manual // check why mode is both in the Monitor and Set for the RC300. It'll be read twice! - // telegram->read_value(hc->mode, 0); // Auto = xFF, Manual = x00 eg. 10 00 FF 08 01 B9 FF + // changed_ |= telegram->read_value(hc->mode, 0); // Auto = xFF, Manual = x00 eg. 10 00 FF 08 01 B9 FF - telegram->read_value(hc->daytemp, 2); // is * 2 - telegram->read_value(hc->nighttemp, 4); // is * 2 + changed_ |= telegram->read_value(hc->daytemp, 2); // is * 2 + changed_ |= telegram->read_value(hc->nighttemp, 4); // is * 2 } // types 0x31D and 0x31E void Thermostat::process_RC300WWmode(std::shared_ptr telegram) { // 0x31D for WW system 1, 0x31E for WW system 2 wwSystem_ = telegram->type_id - 0x31D + 1; - telegram->read_value(wwExtra_, 0); // 0=no, 1=yes + changed_ |= telegram->read_value(wwExtra_, 0); // 0=no, 1=yes // pos 1 = holiday mode // pos 2 = current status of DHW setpoint // pos 3 = current status of DHW circulation pump @@ -1050,15 +1058,15 @@ void Thermostat::process_RC300WWmode(std::shared_ptr telegram) { void Thermostat::process_RC30Monitor(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->setpoint_roomTemp, 1, 1); // is * 2, force as single byte - telegram->read_value(hc->curr_roomTemp, 2); + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 1, 1); // is * 2, force as single byte + changed_ |= telegram->read_value(hc->curr_roomTemp, 2); } // type 0xA7 - for reading the mode from the RC30 thermostat (0x10) void Thermostat::process_RC30Set(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->mode, 23); + changed_ |= telegram->read_value(hc->mode, 23); } // type 0x3E (HC1), 0x48 (HC2), 0x52 (HC3), 0x5C (HC4) - data from the RC35 thermostat (0x10) - 16 bytes @@ -1072,14 +1080,14 @@ void Thermostat::process_RC35Monitor(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->setpoint_roomTemp, 2, 1); // is * 2, force to single byte, is 0 in summermode - telegram->read_value(hc->curr_roomTemp, 3); // is * 10 - or 0x7D00 if thermostat is mounted on boiler + changed_ |= telegram->read_value(hc->setpoint_roomTemp, 2, 1); // is * 2, force to single byte, is 0 in summermode + changed_ |= telegram->read_value(hc->curr_roomTemp, 3); // is * 10 - or 0x7D00 if thermostat is mounted on boiler - telegram->read_bitvalue(hc->mode_type, 1, 1); - telegram->read_bitvalue(hc->summer_mode, 1, 0); - telegram->read_bitvalue(hc->holiday_mode, 0, 5); + changed_ |= telegram->read_bitvalue(hc->mode_type, 1, 1); + changed_ |= telegram->read_bitvalue(hc->summer_mode, 1, 0); + changed_ |= telegram->read_bitvalue(hc->holiday_mode, 0, 5); - telegram->read_value(hc->targetflowtemp, 14); + changed_ |= telegram->read_value(hc->targetflowtemp, 14); } // type 0x3D (HC1), 0x47 (HC2), 0x51 (HC3), 0x5B (HC4) - Working Mode Heating - for reading the mode from the RC35 thermostat (0x10) @@ -1091,16 +1099,16 @@ void Thermostat::process_RC35Set(std::shared_ptr telegram) { std::shared_ptr hc = heating_circuit(telegram); - telegram->read_value(hc->mode, 7); // night, day, auto - telegram->read_value(hc->daytemp, 2); // is * 2 - telegram->read_value(hc->nighttemp, 1); // is * 2 - telegram->read_value(hc->holidaytemp, 3); // is * 2 - telegram->read_value(hc->heatingtype, 0); // 0- off, 1-radiator, 2-convector, 3-floor + changed_ |= telegram->read_value(hc->mode, 7); // night, day, auto + changed_ |= telegram->read_value(hc->daytemp, 2); // is * 2 + changed_ |= telegram->read_value(hc->nighttemp, 1); // is * 2 + changed_ |= telegram->read_value(hc->holidaytemp, 3); // is * 2 + changed_ |= telegram->read_value(hc->heatingtype, 0); // 0- off, 1-radiator, 2-convector, 3-floor - telegram->read_value(hc->summertemp, 22); // is * 1 - telegram->read_value(hc->nofrosttemp, 23); // is * 1 - telegram->read_value(hc->designtemp, 17); // is * 1 - telegram->read_value(hc->offsettemp, 6); // is * 2 + changed_ |= telegram->read_value(hc->summertemp, 22); // is * 1 + changed_ |= telegram->read_value(hc->nofrosttemp, 23); // is * 1 + changed_ |= telegram->read_value(hc->designtemp, 17); // is * 1 + changed_ |= telegram->read_value(hc->offsettemp, 6); // is * 2 } // process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long @@ -1118,6 +1126,7 @@ void Thermostat::process_RCTime(std::shared_ptr telegram) { if (datetime_.empty()) { datetime_.resize(25, '\0'); } + auto timeold = datetime_; // render time to HH:MM:SS DD/MM/YYYY // had to create separate buffers because of how printf works char buf1[6]; @@ -1136,6 +1145,9 @@ void Thermostat::process_RCTime(std::shared_ptr telegram) { Helpers::smallitoa(buf5, telegram->message_data[1]), // month Helpers::itoa(buf6, telegram->message_data[0] + 2000, 10) // year ); + if (timeold != datetime_) { + changed_ = true; + } } // add console commands @@ -1143,15 +1155,9 @@ void Thermostat::console_commands(Shell & shell, unsigned int context) { EMSESPShell::commands->add_command(ShellContext::THERMOSTAT, CommandFlags::ADMIN, flash_string_vector{F_(set), F_(master)}, - flash_string_vector{F_(deviceid_optional)}, + flash_string_vector{F_(deviceid_mandatory)}, [](Shell & shell, const std::vector & arguments) { - uint8_t value; - if (arguments.empty()) { - value = EMSESP_DEFAULT_MASTER_THERMOSTAT; - } else { - value = Helpers::hextoint(arguments.front().c_str()); - } - + uint8_t value = Helpers::hextoint(arguments.front().c_str()); EMSESP::emsespSettingsService.update( [&](EMSESPSettings & settings) { settings.master_thermostat = value; @@ -1203,7 +1209,7 @@ void Thermostat::set_minexttemp(const char * value, const int8_t id) { return; } LOG_INFO(F("Setting min external temperature to %d"), mt); - write_command(EMS_TYPE_IBASettings, 5, mt); + write_command(EMS_TYPE_IBASettings, 5, mt, EMS_TYPE_IBASettings); } // 0xA5 - Clock offset @@ -1213,7 +1219,7 @@ void Thermostat::set_clockoffset(const char * value, const int8_t id) { return; } LOG_INFO(F("Setting clock offset to %d"), co); - write_command(EMS_TYPE_IBASettings, 12, co); + write_command(EMS_TYPE_IBASettings, 12, co, EMS_TYPE_IBASettings); } // 0xA5 - Calibrate internal temperature @@ -1224,7 +1230,7 @@ void Thermostat::set_calinttemp(const char * value, const int8_t id) { } // does this value need to be multiple by 10? LOG_INFO(F("Calibrating internal temperature to %d.%d"), ct / 10, ct < 0 ? -ct % 10 : ct % 10); - write_command(EMS_TYPE_IBASettings, 2, ct); + write_command(EMS_TYPE_IBASettings, 2, ct, EMS_TYPE_IBASettings); } // 0xA5 - Set the display settings @@ -1234,7 +1240,7 @@ void Thermostat::set_display(const char * value, const int8_t id) { return; } LOG_INFO(F("Setting display to %d"), ds); - write_command(EMS_TYPE_IBASettings, 0, ds); + write_command(EMS_TYPE_IBASettings, 0, ds, EMS_TYPE_IBASettings); } void Thermostat::set_remotetemp(const char * value, const int8_t id) { @@ -1271,7 +1277,7 @@ void Thermostat::set_building(const char * value, const int8_t id) { } LOG_INFO(F("Setting building to %d"), bg); - write_command(EMS_TYPE_wwSettings, 6, bg); + write_command(EMS_TYPE_IBASettings, 6, bg, EMS_TYPE_IBASettings); } // 0xA5 Set the language settings @@ -1281,7 +1287,7 @@ void Thermostat::set_language(const char * value, const int8_t id) { return; } LOG_INFO(F("Setting language to %d"), lg); - write_command(EMS_TYPE_wwSettings, 1, lg); + write_command(EMS_TYPE_IBASettings, 1, lg, EMS_TYPE_IBASettings); } // Set the control-mode for hc 0-off, 1-RC20, 2-RC3x @@ -1326,7 +1332,7 @@ void Thermostat::set_wwmode(const char * value, const int8_t id) { if (set != 0xFF) { LOG_INFO(F("Setting thermostat warm water mode to %s"), v.c_str()); - write_command(EMS_TYPE_wwSettings, 2, set); + write_command(EMS_TYPE_wwSettings, 2, set, EMS_TYPE_wwSettings); } else { LOG_WARNING(F("Set thermostat warm water mode: Invalid mode: %s"), v.c_str()); } @@ -1430,7 +1436,7 @@ void Thermostat::set_datetime(const char * value, const int8_t id) { data[7] = (dt[22] - '0') + 2; // DST and flag } LOG_INFO(F("Setting date and time")); - write_command(EMS_TYPE_time, 0, data, 8, 0); + write_command(EMS_TYPE_time, 0, data, 8, EMS_TYPE_time); } // sets the thermostat working mode, where mode is a string @@ -1503,7 +1509,7 @@ void Thermostat::set_mode_n(const uint8_t mode, const uint8_t hc_num) { break; } - switch (this->flags() & 0x0F) { + switch (this->model()) { case EMSdevice::EMS_DEVICE_FLAG_RC20: offset = EMS_OFFSET_RC20Set_mode; validate_typeid = set_typeids[hc_p]; @@ -1557,7 +1563,6 @@ void Thermostat::set_mode_n(const uint8_t mode, const uint8_t hc_num) { // add the write command to the Tx queue // post validate is the corresponding monitor or set type IDs as they can differ per model - // write_command(set_typeids[hc->hc_num() - 1], offset, set_mode_value, validate_typeid); write_command(set_typeids[hc->hc_num() - 1], offset, set_mode_value, validate_typeid); } @@ -1600,7 +1605,7 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co return; } - uint8_t model = this->flags() & 0x0F; + uint8_t model = this->model(); int8_t offset = -1; // we use -1 to check if there is a value uint8_t factor = 2; // some temperatures only use 1 uint16_t validate_typeid = monitor_typeids[hc->hc_num() - 1]; @@ -1616,12 +1621,18 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else if ((model == EMS_DEVICE_FLAG_RC300) || (model == EMS_DEVICE_FLAG_RC100)) { validate_typeid = set_typeids[hc->hc_num() - 1]; - if (mode == HeatingCircuit::Mode::AUTO) { - offset = 0x08; // auto offset - } else if (mode == HeatingCircuit::Mode::MANUAL) { + switch (mode) { + case HeatingCircuit::Mode::MANUAL: offset = 0x0A; // manual offset - } else if (mode == HeatingCircuit::Mode::COMFORT) { + break; + case HeatingCircuit::Mode::COMFORT: offset = 0x02; // comfort offset + break; + default: + case HeatingCircuit::Mode::AUTO: + offset = 0x08; // auto offset + validate_typeid = monitor_typeids[hc->hc_num() - 1]; // get setpoint roomtemp back + break; } } else if (model == EMS_DEVICE_FLAG_RC20_2) { @@ -1632,6 +1643,11 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co case HeatingCircuit::Mode::DAY: // change the day temp offset = EMS_OFFSET_RC20_2_Set_temp_day; break; + default: + case HeatingCircuit::Mode::AUTO: // automatic selection, if no type is defined, we use the standard code + uint8_t mode_type = hc->get_mode_type(this->flags()); + offset = (mode_type == HeatingCircuit::Mode::NIGHT) ? EMS_OFFSET_RC20_2_Set_temp_night : EMS_OFFSET_RC20_2_Set_temp_day; + break; } } else if ((model == EMS_DEVICE_FLAG_RC35) || (model == EMS_DEVICE_FLAG_RC30_1)) { @@ -1662,7 +1678,8 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co factor = 1; break; default: - case HeatingCircuit::Mode::AUTO: // automatic selection, if no type is defined, we use the standard code + case HeatingCircuit::Mode::AUTO: // automatic selection, if no type is defined, we use the standard code + validate_typeid = monitor_typeids[hc->hc_num() - 1]; //get setpoint roomtemp back if (model == EMS_DEVICE_FLAG_RC35) { uint8_t mode_ = hc->get_mode(this->flags()); if (mode_ == HeatingCircuit::Mode::NIGHT) { @@ -1699,8 +1716,13 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co default: case HeatingCircuit::Mode::AUTO: // automatic selection, if no type is defined, we use the standard code uint8_t mode_type = hc->get_mode_type(this->flags()); - offset = (mode_type == HeatingCircuit::Mode::NIGHT || mode_type == HeatingCircuit::Mode::ECO) ? EMS_OFFSET_JunkersSetMessage_night_temp - : EMS_OFFSET_JunkersSetMessage_day_temp; + if (mode_type == HeatingCircuit::Mode::NIGHT || mode_type == HeatingCircuit::Mode::ECO) { + offset = EMS_OFFSET_JunkersSetMessage_night_temp; + } else if (mode_type == HeatingCircuit::Mode::DAY || mode_type == HeatingCircuit::Mode::HEAT) { + offset = EMS_OFFSET_JunkersSetMessage_day_temp; + } else { + offset = EMS_OFFSET_JunkersSetMessage_no_frost_temp; + } break; } @@ -1714,10 +1736,20 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co case HeatingCircuit::Mode::NIGHT: offset = EMS_OFFSET_JunkersSetMessage2_eco_temp; break; - default: case HeatingCircuit::Mode::HEAT: case HeatingCircuit::Mode::DAY: - offset = EMS_OFFSET_JunkersSetMessage3_heat; + offset = EMS_OFFSET_JunkersSetMessage2_heat_temp; + break; + default: + case HeatingCircuit::Mode::AUTO: // automatic selection, if no type is defined, we use the standard code + uint8_t mode_type = hc->get_mode_type(this->flags()); + if (mode_type == HeatingCircuit::Mode::NIGHT || mode_type == HeatingCircuit::Mode::ECO) { + offset = EMS_OFFSET_JunkersSetMessage2_eco_temp; + } else if (mode_type == HeatingCircuit::Mode::DAY || mode_type == HeatingCircuit::Mode::HEAT) { + offset = EMS_OFFSET_JunkersSetMessage2_heat_temp; + } else { + offset = EMS_OFFSET_JunkersSetMessage2_no_frost_temp; + } break; } } @@ -1809,7 +1841,7 @@ void Thermostat::add_commands() { register_mqtt_cmd(F("temp"), [&](const char * value, const int8_t id) { set_temp(value, id); }); register_mqtt_cmd(F("mode"), [&](const char * value, const int8_t id) { set_mode(value, id); }); - uint8_t model = this->flags() & 0x0F; + uint8_t model = this->model(); switch (model) { case EMS_DEVICE_FLAG_RC20_2: register_mqtt_cmd(F("nighttemp"), [&](const char * value, const int8_t id) { set_nighttemp(value, id); }); diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index d230be079..f05a406a6 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -40,10 +40,9 @@ class Thermostat : public EMSdevice { Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand); class HeatingCircuit { public: - HeatingCircuit(const uint8_t hc_num, const uint16_t monitor_typeid, const uint16_t set_typeid) + HeatingCircuit(const uint8_t hc_num) : hc_num_(hc_num) - , monitor_typeid_(monitor_typeid) - , set_typeid_(set_typeid) { + , ha_registered_(false) { } ~HeatingCircuit() = default; @@ -60,42 +59,46 @@ class Thermostat : public EMSdevice { uint8_t targetflowtemp = EMS_VALUE_UINT_NOTSET; uint8_t summertemp = EMS_VALUE_UINT_NOTSET; uint8_t nofrosttemp = EMS_VALUE_UINT_NOTSET; - uint8_t designtemp = EMS_VALUE_UINT_NOTSET; // heatingcurve design temp at MinExtTemp - int8_t offsettemp = EMS_VALUE_INT_NOTSET; // heatingcurve offest temp at roomtemp signed! + uint8_t designtemp = EMS_VALUE_UINT_NOTSET; // heating curve design temp at MinExtTemp + int8_t offsettemp = EMS_VALUE_INT_NOTSET; // heating curve offest temp at roomtemp signed! uint8_t hc_num() const { - return hc_num_; // 1..10 + return hc_num_; + } + + bool ha_registered() const { + return ha_registered_; + } + + void ha_registered(bool b) { + ha_registered_ = b; + } + + // determines if the heating circuit is actually present and has data + bool is_active() { + return Helpers::hasValue(setpoint_roomTemp); } uint8_t get_mode(uint8_t flags) const; uint8_t get_mode_type(uint8_t flags) const; - uint16_t monitor_typeid() const { - return monitor_typeid_; - } - - uint16_t set_typeid() const { - return set_typeid_; - } - enum Mode : uint8_t { UNKNOWN, OFF, MANUAL, AUTO, DAY, NIGHT, HEAT, NOFROST, ECO, HOLIDAY, COMFORT, OFFSET, DESIGN, SUMMER }; - // for sorting + // for sorting based on hc number friend inline bool operator<(const std::shared_ptr & lhs, const std::shared_ptr & rhs) { return (lhs->hc_num_ < rhs->hc_num_); } private: - uint8_t hc_num_; // 1..10 - uint16_t monitor_typeid_; - uint16_t set_typeid_; + uint8_t hc_num_; // heating circuit number 1..10 + bool ha_registered_; // whether it has been registered for HA MQTT Discovery }; static std::string mode_tostring(uint8_t mode); virtual void show_values(uuid::console::Shell & shell); virtual void publish_values(); - virtual void device_info(JsonArray & root); + virtual void device_info_web(JsonArray & root); virtual bool updated_values(); virtual void add_context_menu(); @@ -105,6 +108,11 @@ class Thermostat : public EMSdevice { void console_commands(Shell & shell, unsigned int context); void add_commands(); + // specific thermostat characteristics, stripping the option bits at pos 6 and 7 + inline uint8_t model() const { + return (this->flags() & 0x0F); + } + // each thermostat has a list of heating controller type IDs for reading and writing std::vector monitor_typeids; std::vector set_typeids; @@ -113,10 +121,11 @@ class Thermostat : public EMSdevice { std::string datetime_; // date and time stamp uint8_t mqtt_format_; // single, nested or ha + bool changed_ = false; // Installation parameters uint8_t ibaMainDisplay_ = - EMS_VALUE_UINT_NOTSET; // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 9 smoke temp + EMS_VALUE_UINT_NOTSET; // display on Thermostat: 0 int temp, 1 int setpoint, 2 ext temp, 3 burner temp, 4 ww temp, 5 functioning mode, 6 time, 7 data, 9 smoke temp uint8_t ibaLanguage_ = EMS_VALUE_UINT_NOTSET; // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian int8_t ibaCalIntTemperature_ = EMS_VALUE_INT_NOTSET; // offset int. temperature sensor, by * 0.1 Kelvin (-5.0 to 5.0K) int8_t ibaMinExtTemperature_ = EMS_VALUE_INT_NOTSET; // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 @@ -198,10 +207,9 @@ class Thermostat : public EMSdevice { static constexpr uint8_t EMS_OFFSET_JunkersSetMessage2_set_mode = 4; // EMS offset to set mode on thermostat static constexpr uint8_t EMS_OFFSET_JunkersSetMessage2_no_frost_temp = 5; static constexpr uint8_t EMS_OFFSET_JunkersSetMessage2_eco_temp = 6; - static constexpr uint8_t EMS_OFFSET_JunkersSetMessage3_heat = 7; + static constexpr uint8_t EMS_OFFSET_JunkersSetMessage2_heat_temp = 7; -#define AUTO_HEATING_CIRCUIT 0 -#define DEFAULT_HEATING_CIRCUIT 1 + static constexpr uint8_t AUTO_HEATING_CIRCUIT = 0; // Installation settings static constexpr uint8_t EMS_TYPE_IBASettings = 0xA5; // installation settings @@ -276,7 +284,7 @@ class Thermostat : public EMSdevice { void set_display(const char * value, const int8_t id); void set_building(const char * value, const int8_t id); void set_language(const char * value, const int8_t id); -}; +}; // namespace emsesp } // namespace emsesp diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index b223e33ba..848041542 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -46,7 +46,7 @@ std::string EMSdevice::brand_to_string() const { break; case EMSdevice::Brand::NO_BRAND: default: - return read_flash_string(F("")); + return read_flash_string(F("---")); break; } diff --git a/src/emsdevice.h b/src/emsdevice.h index 6e5025d51..b9160a888 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -140,7 +140,7 @@ class EMSdevice { virtual void publish_values() = 0; virtual bool updated_values() = 0; virtual void add_context_menu() = 0; - virtual void device_info(JsonArray & root) = 0; + virtual void device_info_web(JsonArray & root) = 0; std::string telegram_type_name(std::shared_ptr telegram); diff --git a/src/emsesp.cpp b/src/emsesp.cpp index bff89ea95..a422dbbaf 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -58,6 +58,7 @@ uint8_t EMSESP::actual_master_thermostat_ = EMSESP_DEFAULT_MASTER_THERMOSTAT; / uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; // for when log is TRACE. 0 means no trace set uint8_t EMSESP::watch_ = 0; // trace off uint16_t EMSESP::read_id_ = WATCH_ID_NONE; +uint16_t EMSESP::publish_id_ = 0; bool EMSESP::tap_water_active_ = false; // for when Boiler states we having running warm water. used in Shower() uint32_t EMSESP::last_fetch_ = 0; uint8_t EMSESP::unique_id_count_ = 0; @@ -130,12 +131,7 @@ uint8_t EMSESP::actual_master_thermostat() { // to watch both type IDs and device IDs void EMSESP::watch_id(uint16_t watch_id) { - // if it's a device ID, which is a single byte, remove the MSB so to support both Buderus and HT3 protocols - if (watch_id <= 0xFF) { - watch_id_ = (watch_id & 0x7F); - } else { - watch_id_ = watch_id; - } + watch_id_ = watch_id; } // change the tx_mode @@ -286,27 +282,63 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) { char valuestr[8] = {0}; // for formatting temp shell.printfln(F("Dallas temperature sensors:")); for (const auto & device : sensor_devices()) { - shell.printfln(F(" ID: %s, Temperature: %s°C"), device.to_string().c_str(), Helpers::render_value(valuestr, device.temperature_c, 2)); + shell.printfln(F(" ID: %s, Temperature: %s°C"), device.to_string().c_str(), Helpers::render_value(valuestr, device.temperature_c, 1)); } shell.println(); } -// publish all values from each EMS device to MQTT -// plus the heartbeat and sensor if activated -void EMSESP::publish_all_values() { +void EMSESP::publish_device_values(uint8_t device_type) { if (Mqtt::connected()) { - // Dallas sensors first - sensors_.publish_values(); - - // all the connected EMS devices we known about for (const auto & emsdevice : emsdevices) { - if (emsdevice) { + if (emsdevice && (emsdevice->device_type() == device_type)) { emsdevice->publish_values(); } } } } +void EMSESP::publish_other_values() { + if (Mqtt::connected()) { + for (const auto & emsdevice : emsdevices) { + if (emsdevice && (emsdevice->device_type() != EMSdevice::DeviceType::BOILER) && (emsdevice->device_type() != EMSdevice::DeviceType::THERMOSTAT) + && (emsdevice->device_type() != EMSdevice::DeviceType::SOLAR) && (emsdevice->device_type() != EMSdevice::DeviceType::MIXING)) { + emsdevice->publish_values(); + } + } + } +} + +void EMSESP::publish_sensor_values(const bool force) { + if (Mqtt::connected()) { + if (sensors_.updated_values() || force) { + sensors_.publish_values(); + } + } +} + +// MQTT publish a telegram as raw data +void EMSESP::publish_response(std::shared_ptr telegram) { + StaticJsonDocument doc; + + char buffer[100]; + doc["src"] = Helpers::hextoa(buffer, telegram->src); + doc["dest"] = Helpers::hextoa(buffer, telegram->dest); + doc["type"] = Helpers::hextoa(buffer, telegram->type_id); + doc["offset"] = Helpers::hextoa(buffer, telegram->offset); + strcpy(buffer, Helpers::data_to_hex(telegram->message_data, telegram->message_length).c_str()); + doc["data"] = buffer; + + if (telegram->message_length <= 4) { + uint32_t value = 0; + for (uint8_t i = 0; i < telegram->message_length; i++) { + value = (value << 8) + telegram->message_data[i]; + } + doc["value"] = value; + } + + Mqtt::publish(F("response"), doc); +} + // search for recognized device_ids : Me, All, otherwise print hex value std::string EMSESP::device_tostring(const uint8_t device_id) { if ((device_id & 0x7F) == rxservice_.ems_bus_id()) { @@ -475,9 +507,11 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { // if watching... if (telegram->type_id == read_id_) { LOG_NOTICE(pretty_telegram(telegram).c_str()); + publish_response(telegram); read_id_ = WATCH_ID_NONE; } else if (watch() == WATCH_ON) { - if ((watch_id_ == WATCH_ID_NONE) || (telegram->src == watch_id_) || (telegram->dest == watch_id_) || (telegram->type_id == watch_id_)) { + if ((watch_id_ == WATCH_ID_NONE) || (telegram->type_id == watch_id_) + || ((watch_id_ < 0x80) && ((telegram->src == watch_id_) || (telegram->dest == watch_id_)))) { LOG_NOTICE(pretty_telegram(telegram).c_str()); } } @@ -507,7 +541,10 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { found = emsdevice->handle_telegram(telegram); // check to see if we need to follow up after the telegram has been processed if (found) { - if (emsdevice->updated_values()) { + if ((mqtt_.get_publish_onchange(emsdevice->device_type()) && emsdevice->updated_values()) || telegram->type_id == publish_id_) { + if (telegram->type_id == publish_id_) { + publish_id_ = 0; + } emsdevice->publish_values(); // publish to MQTT if we explicitly have too } } @@ -524,13 +561,14 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { } // calls the device handler's function to populate a json doc with device info -void EMSESP::device_info(const uint8_t unique_id, JsonObject & root) { +// to be used in the Web UI +void EMSESP::device_info_web(const uint8_t unique_id, JsonObject & root) { for (const auto & emsdevice : emsdevices) { if (emsdevice) { if (emsdevice->unique_id() == unique_id) { root["deviceName"] = emsdevice->to_string_short(); // can;t use c_str() because of scope JsonArray data = root.createNestedArray("deviceData"); - emsdevice->device_info(data); + emsdevice->device_info_web(data); return; } } @@ -709,7 +747,6 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { if (tx_state != Telegram::Operation::NONE) { bool tx_successful = false; EMSbus::tx_state(Telegram::Operation::NONE); // reset Tx wait state - // txservice_.print_last_tx(); // if we're waiting on a Write operation, we want a single byte 1 or 4 if ((tx_state == Telegram::Operation::TX_WRITE) && (length == 1)) { @@ -717,7 +754,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { LOG_DEBUG(F("Last Tx write successful")); txservice_.increment_telegram_write_count(); // last tx/write was confirmed ok txservice_.send_poll(); // close the bus - txservice_.post_send_query(); // follow up with any post-read + publish_id_ = txservice_.post_send_query(); // follow up with any post-read if set txservice_.reset_retry_count(); tx_successful = true; } else if (first_value == TxService::TX_WRITE_FAIL) { @@ -771,11 +808,9 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { #ifdef EMSESP_DEBUG LOG_TRACE(F("[DEBUG] Reply after %d ms: %s"), ::millis() - rx_time_, Helpers::data_to_hex(data, length).c_str()); #endif - // check if there is a message for the roomcontroller - Roomctrl::check((data[1] ^ 0x80 ^ rxservice_.ems_mask()), data); - // add to RxQueue, what ever it is. - // in add() the CRC will be checked - rxservice_.add(data, length); + Roomctrl::check((data[1] ^ 0x80 ^ rxservice_.ems_mask()), data); // check if there is a message for the roomcontroller + + rxservice_.add(data, length); // add to RxQueue } } diff --git a/src/emsesp.h b/src/emsesp.h index 67b730302..479a4c35a 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -59,7 +59,9 @@ class EMSESP { static void start(); static void loop(); - static void publish_all_values(); + static void publish_device_values(uint8_t device_type); + static void publish_other_values(); + static void publish_sensor_values(const bool force = false); #ifdef EMSESP_STANDALONE static void run_test(uuid::console::Shell & shell, const std::string & command); // only for testing @@ -84,7 +86,7 @@ class EMSESP { static void send_raw_telegram(const char * data); static bool device_exists(const uint8_t device_id); - static void device_info(const uint8_t unique_id, JsonObject & root); + static void device_info_web(const uint8_t unique_id, JsonObject & root); static uint8_t count_devices(const uint8_t device_type); @@ -174,6 +176,7 @@ class EMSESP { static void process_UBADevices(std::shared_ptr telegram); static void process_version(std::shared_ptr telegram); + static void publish_response(std::shared_ptr telegram); static constexpr uint32_t EMS_FETCH_FREQUENCY = 60000; // check every minute static uint32_t last_fetch_; @@ -191,6 +194,7 @@ class EMSESP { static uint16_t watch_id_; static uint8_t watch_; static uint16_t read_id_; + static uint16_t publish_id_; static bool tap_water_active_; static uint8_t unique_id_count_; diff --git a/src/helpers.cpp b/src/helpers.cpp index 4ca778590..529098bbb 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -329,27 +329,27 @@ bool Helpers::check_abs(const int32_t i) { } // for booleans, use isBool true (EMS_VALUE_BOOL) -bool Helpers::hasValue(const uint8_t v, const uint8_t isBool) { +bool Helpers::hasValue(const uint8_t &v, const uint8_t isBool) { if (isBool == EMS_VALUE_BOOL) { return (v != EMS_VALUE_BOOL_NOTSET); } return (v != EMS_VALUE_UINT_NOTSET); } -bool Helpers::hasValue(const int8_t v) { +bool Helpers::hasValue(const int8_t &v) { return (v != EMS_VALUE_INT_NOTSET); } // for short these are typically 0x8300, 0x7D00 and sometimes 0x8000 -bool Helpers::hasValue(const int16_t v) { +bool Helpers::hasValue(const int16_t &v) { return (abs(v) < EMS_VALUE_USHORT_NOTSET); } -bool Helpers::hasValue(const uint16_t v) { +bool Helpers::hasValue(const uint16_t &v) { return (v < EMS_VALUE_USHORT_NOTSET); } -bool Helpers::hasValue(const uint32_t v) { +bool Helpers::hasValue(const uint32_t &v) { return (v != EMS_VALUE_ULONG_NOTSET); } diff --git a/src/helpers.h b/src/helpers.h index 963c2735f..3f32f1b21 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -50,11 +50,11 @@ class Helpers { static char * ultostr(char * ptr, uint32_t value, const uint8_t base); #endif - static bool hasValue(const uint8_t v, const uint8_t isBool = 0); - static bool hasValue(const int8_t v); - static bool hasValue(const int16_t v); - static bool hasValue(const uint16_t v); - static bool hasValue(const uint32_t v); + static bool hasValue(const uint8_t &v, const uint8_t isBool = 0); + static bool hasValue(const int8_t &v); + static bool hasValue(const int16_t &v); + static bool hasValue(const uint16_t &v); + static bool hasValue(const uint32_t &v); static std::string toLower(std::string const & s); diff --git a/src/locale_EN.h b/src/locale_EN.h index 57f74f86c..548c21d76 100644 --- a/src/locale_EN.h +++ b/src/locale_EN.h @@ -108,7 +108,6 @@ MAKE_PSTR(gpio_mandatory, "") MAKE_PSTR(data_optional, "[data]") MAKE_PSTR(typeid_mandatory, "") MAKE_PSTR(deviceid_mandatory, "") -MAKE_PSTR(deviceid_optional, "[device ID]") MAKE_PSTR(invalid_log_level, "Invalid log level") MAKE_PSTR(log_level_fmt, "Log level = %s") MAKE_PSTR(log_level_optional, "[level]") diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 396bc3783..b33b5b980 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -27,8 +27,13 @@ AsyncMqttClient * Mqtt::mqttClient_; // static parameters we make global std::string Mqtt::hostname_; uint8_t Mqtt::mqtt_qos_; -uint16_t Mqtt::publish_time_; uint8_t Mqtt::bus_id_; +uint32_t Mqtt::publish_time_boiler_; +uint32_t Mqtt::publish_time_thermostat_; +uint32_t Mqtt::publish_time_solar_; +uint32_t Mqtt::publish_time_mixing_; +uint32_t Mqtt::publish_time_other_; +uint32_t Mqtt::publish_time_sensor_; std::vector Mqtt::mqtt_subfunctions_; std::vector Mqtt::mqtt_cmdfunctions_; @@ -111,11 +116,30 @@ void Mqtt::loop() { uint32_t currentMillis = uuid::get_uptime(); // create publish messages for each of the EMS device values, adding to queue - if (publish_time_ && (currentMillis - last_publish_ > publish_time_)) { - last_publish_ = currentMillis; - EMSESP::publish_all_values(); + if (publish_time_boiler_ && (currentMillis - last_publish_boiler_ > publish_time_boiler_)) { + last_publish_boiler_ = currentMillis; + EMSESP::publish_device_values(EMSdevice::DeviceType::BOILER); + } + if (publish_time_thermostat_ && (currentMillis - last_publish_thermostat_ > publish_time_thermostat_)) { + last_publish_thermostat_ = currentMillis; + EMSESP::publish_device_values(EMSdevice::DeviceType::THERMOSTAT); + } + if (publish_time_solar_ && (currentMillis - last_publish_solar_ > publish_time_solar_)) { + last_publish_solar_ = currentMillis; + EMSESP::publish_device_values(EMSdevice::DeviceType::SOLAR); + } + if (publish_time_mixing_ && (currentMillis - last_publish_mixing_ > publish_time_mixing_)) { + last_publish_mixing_ = currentMillis; + EMSESP::publish_device_values(EMSdevice::DeviceType::MIXING); + } + if (publish_time_other_ && (currentMillis - last_publish_other_ > publish_time_other_)) { + last_publish_other_ = currentMillis; + EMSESP::publish_other_values(); + } + if (currentMillis - last_publish_sensor_ > publish_time_sensor_) { + last_publish_sensor_ = currentMillis; + EMSESP::publish_sensor_values(publish_time_sensor_ != 0); } - // publish top item from MQTT queue to stop flooding if ((uint32_t)(currentMillis - last_mqtt_poll_) > MQTT_PUBLISH_WAIT) { last_mqtt_poll_ = currentMillis; @@ -280,7 +304,7 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) { } if (!cmd_known) { - LOG_ERROR(F("MQTT: no matching cmd or invalid data: %s"), command); + LOG_ERROR(F("MQTT: no matching cmd or invalid data: %s"), message); } return; @@ -342,8 +366,13 @@ void Mqtt::start() { // fetch MQTT settings EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & mqttSettings) { - publish_time_ = mqttSettings.publish_time * 1000; // convert to milliseconds - mqtt_qos_ = mqttSettings.mqtt_qos; + publish_time_boiler_ = mqttSettings.publish_time_boiler * 1000; // convert to milliseconds + publish_time_thermostat_ = mqttSettings.publish_time_thermostat * 1000; + publish_time_solar_ = mqttSettings.publish_time_solar * 1000; + publish_time_mixing_ = mqttSettings.publish_time_mixing * 1000; + publish_time_other_ = mqttSettings.publish_time_other * 1000; + publish_time_sensor_ = mqttSettings.publish_time_sensor * 1000; + mqtt_qos_ = mqttSettings.mqtt_qos; }); EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { bus_id_ = settings.ems_bus_id; }); @@ -390,8 +419,51 @@ void Mqtt::start() { mqtt_subfunctions_.reserve(10); } -void Mqtt::set_publish_time(uint16_t publish_time) { - publish_time_ = publish_time * 1000; // convert to milliseconds +void Mqtt::set_publish_time_boiler(uint16_t publish_time) { + publish_time_boiler_ = publish_time * 1000; // convert to milliseconds +} + +void Mqtt::set_publish_time_thermostat(uint16_t publish_time) { + publish_time_thermostat_ = publish_time * 1000; // convert to milliseconds +} + +void Mqtt::set_publish_time_solar(uint16_t publish_time) { + publish_time_solar_ = publish_time * 1000; // convert to milliseconds +} + +void Mqtt::set_publish_time_mixing(uint16_t publish_time) { + publish_time_mixing_ = publish_time * 1000; // convert to milliseconds +} + +void Mqtt::set_publish_time_other(uint16_t publish_time) { + publish_time_other_ = publish_time * 1000; // convert to milliseconds +} + +void Mqtt::set_publish_time_sensor(uint16_t publish_time) { + publish_time_sensor_ = publish_time * 1000; // convert to milliseconds +} + +bool Mqtt::get_publish_onchange(uint8_t device_type) { + if (device_type == EMSdevice::DeviceType::BOILER) { + if (!publish_time_boiler_) { + return true; + } + } else if (device_type == EMSdevice::DeviceType::THERMOSTAT) { + if (!publish_time_thermostat_) { + return true; + } + } else if (device_type == EMSdevice::DeviceType::SOLAR) { + if (!publish_time_solar_) { + return true; + } + } else if (device_type == EMSdevice::DeviceType::MIXING) { + if (!publish_time_mixing_) { + return true; + } + } else if (!publish_time_other_) { + return true; + } + return false; } void Mqtt::set_qos(uint8_t mqtt_qos) { @@ -407,9 +479,9 @@ void Mqtt::on_connect() { #ifndef EMSESP_STANDALONE doc["ip"] = WiFi.localIP().toString(); #endif - publish("info", doc, false); // send with retain off + publish(F("info"), doc, false); // send with retain off - publish("status", "online", true); // say we're alive to the Last Will topic, with retain on + publish(F("status"), "online", true); // say we're alive to the Last Will topic, with retain on reset_publish_fails(); // reset fail count to 0 @@ -466,6 +538,15 @@ void Mqtt::publish(const std::string & topic, const std::string & payload, bool queue_publish_message(topic, payload, retain); } +// MQTT Publish, using a specific retain flag, topic is a flash string +void Mqtt::publish(const __FlashStringHelper * topic, const std::string & payload, bool retain) { + queue_publish_message(uuid::read_flash_string(topic), payload, retain); +} + +void Mqtt::publish(const __FlashStringHelper * topic, const JsonDocument & payload, bool retain) { + publish(uuid::read_flash_string(topic), payload, retain); +} + void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool retain) { std::string payload_text; serializeJson(payload, payload_text); // convert json to string @@ -476,19 +557,15 @@ void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool void Mqtt::publish(const std::string & topic, const bool value) { queue_publish_message(topic, value ? "1" : "0", false); } +void Mqtt::publish(const __FlashStringHelper * topic, const bool value) { + queue_publish_message(uuid::read_flash_string(topic), value ? "1" : "0", false); +} // no payload void Mqtt::publish(const std::string & topic) { queue_publish_message(topic, "", false); } -// publish all queued messages to MQTT -void Mqtt::process_all_queue() { - while (!mqtt_messages_.empty()) { - process_queue(); - } -} - // take top from queue and perform the publish or subscribe action // assumes there is an MQTT connection void Mqtt::process_queue() { diff --git a/src/mqtt.h b/src/mqtt.h index fb8dac748..e37b6502a 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -67,8 +67,14 @@ class Mqtt { void loop(); void start(); - void set_publish_time(uint16_t publish_time); + void set_publish_time_boiler(uint16_t publish_time); + void set_publish_time_thermostat(uint16_t publish_time); + void set_publish_time_solar(uint16_t publish_time); + void set_publish_time_mixing(uint16_t publish_time); + void set_publish_time_other(uint16_t publish_time); + void set_publish_time_sensor(uint16_t publish_time); void set_qos(uint8_t mqtt_qos); + bool get_publish_onchange(uint8_t device_type); enum Operation { PUBLISH, SUBSCRIBE }; @@ -82,7 +88,10 @@ class Mqtt { static void publish(const std::string & topic, const std::string & payload, bool retain = false); static void publish(const std::string & topic, const JsonDocument & payload, bool retain = false); + static void publish(const __FlashStringHelper * topic, const JsonDocument & payload, bool retain = false); + static void publish(const __FlashStringHelper * topic, const std::string & payload, bool retain = false); static void publish(const std::string & topic, const bool value); + static void publish(const __FlashStringHelper * topi, const bool value); static void publish(const std::string & topic); static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type); @@ -167,7 +176,6 @@ class Mqtt { void on_publish(uint16_t packetId); void on_message(const char * topic, const char * payload, size_t len); void process_queue(); - void process_all_queue(); static uint16_t mqtt_publish_fails_; @@ -189,14 +197,25 @@ class Mqtt { static std::vector mqtt_subfunctions_; // list of mqtt subscribe callbacks for all devices static std::vector mqtt_cmdfunctions_; // list of commands - uint32_t last_mqtt_poll_ = 0; - uint32_t last_publish_ = 0; + uint32_t last_mqtt_poll_ = 0; + uint32_t last_publish_boiler_ = 0; + uint32_t last_publish_thermostat_ = 0; + uint32_t last_publish_solar_ = 0; + uint32_t last_publish_mixing_ = 0; + uint32_t last_publish_other_ = 0; + uint32_t last_publish_sensor_ = 0; // settings, copied over static std::string hostname_; static uint8_t mqtt_qos_; - static uint16_t publish_time_; + static uint32_t publish_time_; static uint8_t bus_id_; + static uint32_t publish_time_boiler_; + static uint32_t publish_time_thermostat_; + static uint32_t publish_time_solar_; + static uint32_t publish_time_mixing_; + static uint32_t publish_time_other_; + static uint32_t publish_time_sensor_; }; } // namespace emsesp diff --git a/src/sensors.cpp b/src/sensors.cpp index 2b8256246..79ae8de9c 100644 --- a/src/sensors.cpp +++ b/src/sensors.cpp @@ -49,7 +49,10 @@ void Sensors::reload() { mqtt_format_ = settings.mqtt_format; // single, nested or ha }); - EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { dallas_gpio_ = settings.dallas_gpio; }); + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { + dallas_gpio_ = settings.dallas_gpio; + parasite_ = settings.dallas_parasite; + }); if (mqtt_format_ == MQTT_format::HA) { for (uint8_t i = 0; i < MAX_SENSORS; registered_ha_[i++] = false) @@ -64,10 +67,10 @@ void Sensors::loop() { if (state_ == State::IDLE) { if (time_now - last_activity_ >= READ_INTERVAL_MS) { // LOG_DEBUG(F("Read sensor temperature")); // uncomment for debug - if (bus_.reset()) { + if (bus_.reset() || parasite_) { YIELD; bus_.skip(); - bus_.write(CMD_CONVERT_TEMP); + bus_.write(CMD_CONVERT_TEMP, parasite_ ? 1 : 0); state_ = State::READING; } else { // no sensors found @@ -94,8 +97,9 @@ void Sensors::loop() { uint8_t addr[ADDR_LEN] = {0}; if (bus_.search(addr)) { - bus_.depower(); - + if (!parasite_) { + bus_.depower(); + } if (bus_.crc8(addr, ADDR_LEN - 1) == addr[ADDR_LEN - 1]) { switch (addr[0]) { case TYPE_DS18B20: @@ -122,8 +126,19 @@ void Sensors::loop() { LOG_ERROR(F("Invalid sensor %s"), Device(addr).to_string().c_str()); } } else { - bus_.depower(); + if (!parasite_) { + bus_.depower(); + } if ((found_.size() >= devices_.size()) || (retrycnt_ > 5)) { + if (found_.size() == devices_.size()) { + for (uint8_t i = 0; i < devices_.size(); i++) { + if (found_[i].temperature_c != devices_[i].temperature_c) { + changed_ = true; + } + } + } else { + changed_ = true; + } devices_ = std::move(found_); retrycnt_ = 0; } else { @@ -140,9 +155,12 @@ void Sensors::loop() { bool Sensors::temperature_convert_complete() { #ifndef EMSESP_STANDALONE + if (parasite_) { + return true; // don't care, use the minimum time in loop + } return bus_.read_bit() == 1; #else - return 1; + return true; #endif } @@ -183,27 +201,27 @@ float Sensors::get_temperature_c(const uint8_t addr[]) { int16_t raw_value = ((int16_t)scratchpad[SCRATCHPAD_TEMP_MSB] << 8) | scratchpad[SCRATCHPAD_TEMP_LSB]; - // Adjust based on device resolution - int resolution = 9 + ((scratchpad[SCRATCHPAD_CONFIG] >> 5) & 0x3); - switch (resolution) { - case 9: - raw_value &= ~0x1; - break; - - case 10: - raw_value &= ~0x3; - break; - - case 11: - raw_value &= ~0x7; - break; - - case 12: - break; + if (addr[0] == TYPE_DS18S20) { + raw_value = (raw_value << 3) + 12 - scratchpad[SCRATCHPAD_CNT_REM]; + } else { + // Adjust based on device resolution + int resolution = 9 + ((scratchpad[SCRATCHPAD_CONFIG] >> 5) & 0x3); + switch (resolution) { + case 9: + raw_value &= ~0x7; + break; + case 10: + raw_value &= ~0x3; + break; + case 11: + raw_value &= ~0x1; + break; + case 12: + break; + } } - - uint32_t raw = (raw_value * 625) / 100; // round to 0.01 - return (float)raw / 100; + uint32_t raw = (raw_value * 625 + 500) / 1000; // round to 0.1 + return (float)raw / 10; #else return NAN; #endif @@ -237,6 +255,14 @@ std::string Sensors::Device::to_string() const { return str; } +// check to see if values have been updated +bool Sensors::updated_values() { + if (changed_) { + changed_ = false; + return true; + } + return false; +} // send all dallas sensor values as a JSON package to MQTT // assumes there are devices @@ -254,7 +280,7 @@ void Sensors::publish_values() { StaticJsonDocument<100> doc; for (const auto & device : devices_) { char s[7]; // sensorrange -55.00 to 125.00 - doc["temp"] = Helpers::render_value(s, device.temperature_c, 2); + doc["temp"] = Helpers::render_value(s, device.temperature_c, 1); char topic[60]; // sensors{1-n} strlcpy(topic, "sensor_", 50); // create topic, e.g. home/ems-esp/sensor_28-EA41-9497-0E03-5F strlcat(topic, device.to_string().c_str(), 60); @@ -264,28 +290,22 @@ void Sensors::publish_values() { return; } - // const size_t capacity = num_devices * JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(num_devices); DynamicJsonDocument doc(100 * num_devices); uint8_t i = 1; // sensor count for (const auto & device : devices_) { char s[7]; if (mqtt_format_ == MQTT_format::CUSTOM) { - doc[device.to_string()] = Helpers::render_value(s, device.temperature_c, 2); - } else if (mqtt_format_ == MQTT_format::SINGLE) { - doc["id"] = device.to_string(); - doc["temp"] = Helpers::render_value(s, device.temperature_c, 2); - std::string topic(100, '\0'); - snprintf_P(&topic[0], 50, PSTR("sensor%d"), i); - Mqtt::publish(topic, doc); + // e.g. sensors = {28-EA41-9497-0E03-5F":23.30,"28-233D-9497-0C03-8B":24.0} + doc[device.to_string()] = Helpers::render_value(s, device.temperature_c, 1); } else if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::HA)) { - // e.g. {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.30"},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":"24.0"}} + // e.g. sensors = {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.30"},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":"24.0"}} char sensorID[10]; // sensor{1-n} strlcpy(sensorID, "sensor", 10); strlcat(sensorID, Helpers::itoa(s, i), 10); JsonObject dataSensor = doc.createNestedObject(sensorID); dataSensor["id"] = device.to_string(); - dataSensor["temp"] = Helpers::render_value(s, device.temperature_c, 2); + dataSensor["temp"] = Helpers::render_value(s, device.temperature_c, 1); } // special for HA @@ -327,9 +347,9 @@ void Sensors::publish_values() { } if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::CUSTOM)) { - Mqtt::publish("sensors", doc); + Mqtt::publish(F("sensors"), doc); } else if (mqtt_format_ == MQTT_format::HA) { - Mqtt::publish("homeassistant/sensor/ems-esp/state", doc); + Mqtt::publish(F("homeassistant/sensor/ems-esp/state"), doc); } } } // namespace emsesp \ No newline at end of file diff --git a/src/sensors.h b/src/sensors.h index 6fcaba010..8da3a18a4 100644 --- a/src/sensors.h +++ b/src/sensors.h @@ -60,6 +60,7 @@ class Sensors { void loop(); void publish_values(); void reload(); + bool updated_values(); const std::vector devices() const; @@ -74,12 +75,13 @@ class Sensors { static constexpr size_t SCRATCHPAD_TEMP_MSB = 1; static constexpr size_t SCRATCHPAD_TEMP_LSB = 0; static constexpr size_t SCRATCHPAD_CONFIG = 4; + static constexpr size_t SCRATCHPAD_CNT_REM = 6; // dallas chips static constexpr uint8_t TYPE_DS18B20 = 0x28; static constexpr uint8_t TYPE_DS18S20 = 0x10; static constexpr uint8_t TYPE_DS1822 = 0x22; - static constexpr uint8_t TYPE_DS1825 = 0x3B; + static constexpr uint8_t TYPE_DS1825 = 0x3B; // also DS1826 static constexpr uint32_t READ_INTERVAL_MS = 5000; // 5 seconds static constexpr uint32_t CONVERSION_MS = 1000; // 1 seconds @@ -109,6 +111,8 @@ class Sensors { uint8_t mqtt_format_; uint8_t retrycnt_ = 0; uint8_t dallas_gpio_ = 0; + bool parasite_ = false; + bool changed_ = false; }; } // namespace emsesp diff --git a/src/shower.cpp b/src/shower.cpp index 2d8e33ef2..4451e0a71 100644 --- a/src/shower.cpp +++ b/src/shower.cpp @@ -53,7 +53,7 @@ void Shower::loop() { // first check to see if hot water has been on long enough to be recognized as a Shower/Bath if (!shower_on_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) { shower_on_ = true; - Mqtt::publish("shower_active", (bool)true); + Mqtt::publish(F("shower_active"), (bool)true); LOG_DEBUG(F("[Shower] hot water still running, starting shower timer")); } // check if the shower has been on too long @@ -74,7 +74,7 @@ void Shower::loop() { if ((timer_pause_ - timer_start_) > SHOWER_OFFSET_TIME) { duration_ = (timer_pause_ - timer_start_ - SHOWER_OFFSET_TIME); if (duration_ > SHOWER_MIN_DURATION) { - Mqtt::publish("shower_active", (bool)false); + Mqtt::publish(F("shower_active"), (bool)false); LOG_DEBUG(F("[Shower] finished with duration %d"), duration_); publish_values(); } @@ -129,7 +129,7 @@ void Shower::publish_values() { doc["duration"] = s; } - Mqtt::publish("shower_data", doc); + Mqtt::publish(F("shower_data"), doc); } } // namespace emsesp diff --git a/src/system.cpp b/src/system.cpp index beec9d28f..ba5709e33 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -35,6 +35,7 @@ int System::reset_counter_ = 0; bool System::upload_status_ = false; bool System::hide_led_ = false; uint8_t System::led_gpio_ = 0; +uint16_t System::analog_ = 0; // send on/off to a gpio pin // value: true = HIGH, false = LOW @@ -188,6 +189,7 @@ void System::loop() { #endif led_monitor(); // check status and report back using the LED system_check(); // check system health + measure_analog(); // send out heartbeat uint32_t currentMillis = uuid::get_uptime(); @@ -233,8 +235,34 @@ void System::send_heartbeat() { doc["mqttpublishfails"] = Mqtt::publish_fails(); doc["txfails"] = EMSESP::txservice_.telegram_fail_count(); doc["rxfails"] = EMSESP::rxservice_.telegram_error_count(); + doc["adc"] = analog_; //analogRead(A0); - Mqtt::publish("heartbeat", doc, false); // send to MQTT with retain off. This will add to MQTT queue. + Mqtt::publish(F("heartbeat"), doc, false); // send to MQTT with retain off. This will add to MQTT queue. +} + +// measure and moving average adc +void System::measure_analog() { + static uint32_t measure_last_ = 0; + + if (!measure_last_ || (uint32_t)(uuid::get_uptime() - measure_last_) >= SYSTEM_MEASURE_ANALOG_INTERVAL) { + measure_last_ = uuid::get_uptime(); +#if defined(ESP8266) + uint16_t a = analogRead(A0); +#elif defined(ESP32) + uint16_t a = analogRead(36); +#else + uint16_t a = 0; // standalone +#endif + static uint32_t sum_ = 0; + + if (!analog_) { // init first time + analog_ = a; + sum_ = a * 256; + } else { // simple moving average filter + sum_ = sum_ * 255 / 256 + a; + analog_ = sum_ / 256; + } + } } // sets rate of led flash @@ -594,9 +622,23 @@ void System::console_commands(Shell & shell, unsigned int context) { // upgrade from previous versions of EMS-ESP, based on SPIFFS on an ESP8266 // returns true if an upgrade was done +// the logic is bit abnormal (loading both filesystems and testing) but this was the only way I could get it to work reliably bool System::check_upgrade() { #if defined(ESP8266) + LittleFSConfig l_cfg; + l_cfg.setAutoFormat(false); + LittleFS.setConfig(l_cfg); // do not auto format if it can't find LittleFS + if (LittleFS.begin()) { +#if defined(EMSESP_DEBUG) + Serial.begin(115200); + Serial.println(F("FS is Littlefs")); + Serial.flush(); + Serial.end(); +#endif + return false; + } + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" @@ -604,120 +646,172 @@ bool System::check_upgrade() { cfg.setAutoFormat(false); // prevent formatting when opening SPIFFS filesystem SPIFFS.setConfig(cfg); if (!SPIFFS.begin()) { - return false; // is not SPIFFS +#if defined(EMSESP_DEBUG) + Serial.begin(115200); + Serial.println(F("No old SPIFFS found!")); + Serial.flush(); + Serial.end(); +#endif + return false; } Serial.begin(115200); - // open the two files - File file1 = SPIFFS.open("/myesp.json", "r"); - File file2 = SPIFFS.open("/customconfig.json", "r"); - if (!file1 || !file2) { - Serial.println(F("Unable to read the config files")); - file1.close(); - file2.close(); - SPIFFS.end(); - return false; // can't open files - } - - // read the content of the files - DeserializationError error; - StaticJsonDocument<1024> doc1; // for myESP settings - StaticJsonDocument<1024> doc2; // for custom EMS-ESP settings bool failed = false; + File file; + JsonObject network, general, mqtt, custom_settings; + StaticJsonDocument<1024> doc; - error = deserializeJson(doc1, file1); - if (error) { - Serial.printf("Error. Failed to deserialize json, doc1, error %s", error.c_str()); + // open the system settings: + // { + // "command":"configfile", + // "network":{"ssid":"xxxx","password":"yyyy","wmode":1,"staticip":null,"gatewayip":null,"nmask":null,"dnsip":null}, + // "general":{"password":"admin","serial":false,"hostname":"ems-esp","log_events":false,"log_ip":null,"version":"1.9.5"}, + // "mqtt":{"enabled":false,"heartbeat":false,"ip":null,"user":null,"port":1883,"qos":0,"keepalive":60,"retain":false,"password":null,"base":null,"nestedjson":false}, + // "ntp":{"server":"pool.ntp.org","interval":720,"enabled":false,"timezone":2} + // } + file = SPIFFS.open("/myesp.json", "r"); + if (!file) { + Serial.println(F("Unable to read the system config file")); failed = true; + } else { + DeserializationError error = deserializeJson(doc, file); + if (error) { + Serial.printf(PSTR("Error. Failed to deserialize system json, error %s\n"), error.c_str()); + failed = true; + } else { + Serial.println(F("Migrating settings from EMS-ESP v1.9...")); +#if defined(EMSESP_DEBUG) + serializeJson(doc, Serial); + Serial.println(); +#endif + network = doc["network"]; + general = doc["general"]; + mqtt = doc["mqtt"]; + + // start up LittleFS. If it doesn't exist it will format it + l_cfg.setAutoFormat(true); + LittleFS.setConfig(l_cfg); + LittleFS.begin(); + EMSESP::esp8266React.begin(); + EMSESP::emsespSettingsService.begin(); + + EMSESP::esp8266React.getWiFiSettingsService()->update( + [&](WiFiSettings & wifiSettings) { + wifiSettings.hostname = general["hostname"] | FACTORY_WIFI_HOSTNAME; + wifiSettings.ssid = network["ssid"] | FACTORY_WIFI_SSID; + wifiSettings.password = network["password"] | FACTORY_WIFI_PASSWORD; + + wifiSettings.staticIPConfig = false; + JsonUtils::readIP(network, "staticip", wifiSettings.localIP); + JsonUtils::readIP(network, "dnsip", wifiSettings.dnsIP1); + JsonUtils::readIP(network, "gatewayip", wifiSettings.gatewayIP); + JsonUtils::readIP(network, "nmask", wifiSettings.subnetMask); + + return StateUpdateResult::CHANGED; + }, + "local"); + + EMSESP::esp8266React.getSecuritySettingsService()->update( + [&](SecuritySettings & securitySettings) { + securitySettings.jwtSecret = general["password"] | FACTORY_JWT_SECRET; + + return StateUpdateResult::CHANGED; + }, + "local"); + + EMSESP::esp8266React.getMqttSettingsService()->update( + [&](MqttSettings & mqttSettings) { + mqttSettings.host = mqtt["ip"] | FACTORY_MQTT_HOST; + mqttSettings.mqtt_format = (mqtt["nestedjson"] ? MQTT_format::NESTED : MQTT_format::SINGLE); + mqttSettings.mqtt_qos = mqtt["qos"] | 0; + mqttSettings.username = mqtt["user"] | ""; + mqttSettings.password = mqtt["password"] | ""; + mqttSettings.port = mqtt["port"] | FACTORY_MQTT_PORT; + mqttSettings.clientId = FACTORY_MQTT_CLIENT_ID; + mqttSettings.enabled = mqtt["enabled"]; + mqttSettings.system_heartbeat = mqtt["heartbeat"]; + mqttSettings.keepAlive = FACTORY_MQTT_KEEP_ALIVE; + mqttSettings.cleanSession = FACTORY_MQTT_CLEAN_SESSION; + mqttSettings.maxTopicLength = FACTORY_MQTT_MAX_TOPIC_LENGTH; + + return StateUpdateResult::CHANGED; + }, + "local"); + } } - error = deserializeJson(doc2, file2); - if (error) { - Serial.printf("Error. Failed to deserialize json, doc2, error %s", error.c_str()); - failed = true; + file.close(); + + if (failed) { +#if defined(EMSESP_DEBUG) + Serial.println(F("Failed to read system config. Quitting.")); +#endif + SPIFFS.end(); + Serial.end(); + return false; } - file1.close(); - file2.close(); + // open the custom settings file next: + // { + // "command":"custom_configfile", + // "settings":{"led":true,"led_gpio":2,"dallas_gpio":14,"dallas_parasite":false,"listen_mode":false,"shower_timer":false,"shower_alert":false,"publish_time":0,"tx_mode":1,"bus_id":11,"master_thermostat":0,"known_devices":""} + // } + doc.clear(); + failed = false; + file = SPIFFS.open("/customconfig.json", "r"); + if (!file) { + Serial.println(F("Unable to read custom config file")); + failed = true; + } else { + DeserializationError error = deserializeJson(doc, file); + if (error) { + Serial.printf(PSTR("Error. Failed to deserialize custom json, error %s\n"), error.c_str()); + failed = true; + } else { +#if defined(EMSESP_DEBUG) + serializeJson(doc, Serial); + Serial.println(); +#endif + custom_settings = doc["settings"]; + EMSESP::emsespSettingsService.update( + [&](EMSESPSettings & settings) { + settings.tx_mode = custom_settings["tx_mode"] | EMSESP_DEFAULT_TX_MODE; + settings.shower_alert = custom_settings["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT; + settings.shower_timer = custom_settings["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER; + settings.master_thermostat = custom_settings["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT; + settings.ems_bus_id = custom_settings["bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID; + settings.syslog_host = EMSESP_DEFAULT_SYSLOG_HOST; + settings.syslog_level = EMSESP_DEFAULT_SYSLOG_LEVEL; + settings.syslog_mark_interval = EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL; + settings.dallas_gpio = custom_settings["dallas_gpio"] | EMSESP_DEFAULT_DALLAS_GPIO; + settings.dallas_parasite = custom_settings["dallas_parasite"] | EMSESP_DEFAULT_DALLAS_PARASITE; + settings.led_gpio = custom_settings["led_gpio"] | EMSESP_DEFAULT_LED_GPIO; + + return StateUpdateResult::CHANGED; + }, + "local"); + } + } + file.close(); + SPIFFS.end(); if (failed) { - return false; // parse error +#if defined(EMSESP_DEBUG) + Serial.println(F("Failed to read custom config. Quitting.")); +#endif + Serial.end(); + return false; } #pragma GCC diagnostic pop - LittleFS.begin(); - EMSESP::esp8266React.begin(); // loads system settings (wifi, mqtt, etc) - EMSESP::emsespSettingsService.begin(); // load EMS-ESP specific settings - - Serial.println(F("Migrating settings from EMS-ESP 1.9.x...")); - - // get the json objects - JsonObject network = doc1["network"]; - JsonObject general = doc1["general"]; - JsonObject mqtt = doc1["mqtt"]; - JsonObject custom_settings = doc2["settings"]; // from 2nd file - - EMSESP::esp8266React.getWiFiSettingsService()->update( - [&](WiFiSettings & wifiSettings) { - wifiSettings.hostname = general["hostname"] | FACTORY_WIFI_HOSTNAME; - wifiSettings.ssid = network["ssid"] | FACTORY_WIFI_SSID; - wifiSettings.password = network["password"] | FACTORY_WIFI_PASSWORD; - - wifiSettings.staticIPConfig = false; - JsonUtils::readIP(network, "staticip", wifiSettings.localIP); - JsonUtils::readIP(network, "dnsip", wifiSettings.dnsIP1); - JsonUtils::readIP(network, "gatewayip", wifiSettings.gatewayIP); - JsonUtils::readIP(network, "nmask", wifiSettings.subnetMask); - - return StateUpdateResult::CHANGED; - }, - "local"); - - EMSESP::esp8266React.getMqttSettingsService()->update( - [&](MqttSettings & mqttSettings) { - mqttSettings.host = mqtt["ip"] | FACTORY_MQTT_HOST; - mqttSettings.mqtt_format = (mqtt["nestedjson"] ? 2 : 1); - mqttSettings.mqtt_qos = mqtt["qos"] | 0; - mqttSettings.username = mqtt["user"] | ""; - mqttSettings.password = mqtt["password"] | ""; - mqttSettings.port = mqtt["port"] | FACTORY_MQTT_PORT; - mqttSettings.clientId = FACTORY_MQTT_CLIENT_ID; - mqttSettings.enabled = mqtt["enabled"]; - mqttSettings.system_heartbeat = mqtt["heartbeat"]; - mqttSettings.keepAlive = FACTORY_MQTT_KEEP_ALIVE; - mqttSettings.cleanSession = FACTORY_MQTT_CLEAN_SESSION; - mqttSettings.maxTopicLength = FACTORY_MQTT_MAX_TOPIC_LENGTH; - - return StateUpdateResult::CHANGED; - }, - "local"); - - EMSESP::esp8266React.getSecuritySettingsService()->update( - [&](SecuritySettings & securitySettings) { - securitySettings.jwtSecret = general["password"] | FACTORY_JWT_SECRET; - - return StateUpdateResult::CHANGED; - }, - "local"); - - EMSESP::emsespSettingsService.update( - [&](EMSESPSettings & settings) { - settings.tx_mode = custom_settings["tx_mode"] | EMSESP_DEFAULT_TX_MODE; - settings.shower_alert = custom_settings["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT; - settings.shower_timer = custom_settings["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER; - settings.master_thermostat = custom_settings["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT; - settings.ems_bus_id = custom_settings["bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID; - settings.syslog_host = EMSESP_DEFAULT_SYSLOG_HOST; - settings.syslog_level = EMSESP_DEFAULT_SYSLOG_LEVEL; - settings.syslog_mark_interval = EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL; - - return StateUpdateResult::CHANGED; - }, - "local"); - + Serial.println(F("Restarting...")); + Serial.flush(); + delay(1000); Serial.end(); + delay(1000); + restart(); return true; #else return false; diff --git a/src/system.h b/src/system.h index 64e2e48ef..c63dd9f07 100644 --- a/src/system.h +++ b/src/system.h @@ -68,29 +68,19 @@ class System { static uuid::syslog::SyslogService syslog_; #endif - static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // check every 5 seconds - static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection, 1 sec - static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence - static constexpr uint32_t SYSTEM_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min) + static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // check every 5 seconds + static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection, 1 sec + static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence + static constexpr uint32_t SYSTEM_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min) + static constexpr uint32_t SYSTEM_MEASURE_ANALOG_INTERVAL = 1100; -// internal LED -#ifndef EMSESP_NO_LED -#if defined(ESP8266) + // internal LED static constexpr uint8_t LED_ON = LOW; -#elif defined(ESP32) -#ifdef WEMOS_D1_32 - static constexpr uint8_t LED_ON = HIGH; -#else - static constexpr uint8_t LED_ON = LOW; -#endif -#endif -#else - static constexpr uint8_t LED_ON = 0; -#endif void led_monitor(); void set_led_speed(uint32_t speed); void system_check(); + void measure_analog(); static void show_system(uuid::console::Shell & shell); static void show_users(uuid::console::Shell & shell); @@ -103,6 +93,7 @@ class System { static int reset_counter_; uint32_t last_heartbeat_ = 0; static bool upload_status_; // true if we're in the middle of a OTA firmware upload + static uint16_t analog_; // settings bool system_heartbeat_; diff --git a/src/telegram.cpp b/src/telegram.cpp index 5b22c26ed..7ae63d7eb 100644 --- a/src/telegram.cpp +++ b/src/telegram.cpp @@ -73,7 +73,7 @@ Telegram::Telegram(const uint8_t operation, , offset(offset) , message_length(message_length) { // copy complete telegram data over, preventing buffer overflow - for (uint8_t i = 0; ((i < message_length) && (i != EMS_MAX_TELEGRAM_MESSAGE_LENGTH - 1)); i++) { + for (uint8_t i = 0; ((i < message_length) && (i < EMS_MAX_TELEGRAM_MESSAGE_LENGTH)); i++) { message_data[i] = data[i]; } } @@ -95,7 +95,7 @@ std::string Telegram::to_string() const { data[2] = this->type_id; length = 5; } - } else if (this->operation == Telegram::Operation::TX_WRITE) { + } else { data[1] = this->dest; if (this->type_id > 0xFF) { data[2] = 0xFF; @@ -109,10 +109,6 @@ std::string Telegram::to_string() const { for (uint8_t i = 0; i < this->message_length; i++) { data[length++] = this->message_data[i]; } - } else { - for (uint8_t i = 0; i < this->message_length; i++) { - data[length++] = this->message_data[i]; - } } return Helpers::data_to_hex(data, length); @@ -189,13 +185,14 @@ void RxService::add(uint8_t * data, uint8_t length) { } type_id = (data[4 + shift] << 8) + data[5 + shift] + 256; message_data = data + 6 + shift; - message_length = length - 6 - shift; + message_length = length - 7 - shift; } // if we're watching and "raw" print out actual telegram as bytes to the console if (EMSESP::watch() == EMSESP::Watch::WATCH_RAW) { uint16_t trace_watch_id = EMSESP::watch_id(); - if ((trace_watch_id == WATCH_ID_NONE) || (src == trace_watch_id) || (dest == trace_watch_id) || (type_id == trace_watch_id)) { + if ((trace_watch_id == WATCH_ID_NONE) || (type_id == trace_watch_id) + || ((trace_watch_id < 0x80) && ((src == trace_watch_id) || (dest == trace_watch_id)))) { LOG_NOTICE(F("Rx: %s"), Helpers::data_to_hex(data, length).c_str()); } } @@ -213,16 +210,16 @@ void RxService::add(uint8_t * data, uint8_t length) { // if we receive a hc2.. telegram from 0x19.. match it to master_thermostat if master is 0x18 src = EMSESP::check_master_device(src, type_id, true); - // create the telegram + // create the telegram auto telegram = std::make_shared(Telegram::Operation::RX, src, dest, type_id, offset, message_data, message_length); // check if queue is full, if so remove top item to make space if (rx_telegrams_.size() >= MAX_RX_TELEGRAMS) { rx_telegrams_.pop_front(); + increment_telegram_error_count(); } rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram)); // add to queue - } // @@ -319,7 +316,7 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { telegram_raw[2] = 0xFF; // fixed value indicating an extended message telegram_raw[3] = telegram->offset; - // EMS+ has different format for read and write. See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats + // EMS+ has different format for read and write if (telegram->operation == Telegram::Operation::TX_WRITE) { // WRITE telegram_raw[4] = (telegram->type_id >> 8) - 1; // type, 1st byte, high-byte, subtract 0x100 @@ -472,7 +469,9 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt operation = Telegram::Operation::TX_READ; } else { operation = Telegram::Operation::TX_WRITE; + set_post_send_query(type_id); } + EMSESP::set_read_id(type_id); } auto telegram = std::make_shared(operation, src, dest, type_id, offset, message_data, message_length); // operation is TX_WRITE or TX_READ @@ -535,7 +534,7 @@ void TxService::send_raw(const char * telegram_data) { return; // nothing to send } - add(Telegram::Operation::TX_RAW, data, count + 1); // add to Tx queue + add(Telegram::Operation::TX_RAW, data, count + 1, true); // add to front of Tx queue } // add last Tx to tx queue and increment count @@ -578,21 +577,22 @@ bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const { } // sends a type_id read request to fetch values after a successful Tx write operation -void TxService::post_send_query() { - if (telegram_last_post_send_query_) { - uint8_t dest = (telegram_last_->dest & 0x7F); - uint8_t message_data[1] = {EMS_MAX_TELEGRAM_LENGTH}; // request all data, 32 bytes - add(Telegram::Operation::TX_READ, dest, telegram_last_post_send_query_, 0, message_data, 1, true); - // read_request(telegram_last_post_send_query_, dest, 0); // no offset - LOG_DEBUG(F("Sending post validate read, type ID 0x%02X to dest 0x%02X"), telegram_last_post_send_query_, dest); - } -} +// unless the post_send_query has a type_id of 0 +uint16_t TxService::post_send_query() { + uint16_t post_typeid = this->get_post_send_query(); -// print out the last Tx that was sent -void TxService::print_last_tx() { - LOG_DEBUG(F("Last Tx %s operation: %s"), - (telegram_last_->operation == Telegram::Operation::TX_WRITE) ? F("Write") : F("Read"), - telegram_last_->to_string().c_str()); + if (post_typeid) { + uint8_t dest = (this->telegram_last_->dest & 0x7F); + // when set a value with large offset before and validate on same type, we have to add offset 0, 26, 52, ... + uint8_t offset = (this->telegram_last_->type_id == post_typeid) ? ((this->telegram_last_->offset / 26) * 26) : 0; + uint8_t message_data[1] = {EMS_MAX_TELEGRAM_LENGTH}; // request all data, 32 bytes + this->add(Telegram::Operation::TX_READ, dest, post_typeid, offset, message_data, 1, true); + // read_request(telegram_last_post_send_query_, dest, 0); // no offset + LOG_DEBUG(F("Sending post validate read, type ID 0x%02X to dest 0x%02X"), post_typeid, dest); + set_post_send_query(0); // reset + } + + return post_typeid; } } // namespace emsesp diff --git a/src/telegram.h b/src/telegram.h index 1eca7bd82..40234ee23 100644 --- a/src/telegram.h +++ b/src/telegram.h @@ -83,36 +83,43 @@ class Telegram { std::string to_string() const; // reads a bit value from a given telegram position - void read_bitvalue(uint8_t & value, const uint8_t index, const uint8_t bit) const { - uint8_t abs_index = (index - offset); - if (abs_index >= message_length - 1) { - return; // out of bounds + bool read_bitvalue(uint8_t & value, const uint8_t index, const uint8_t bit) const { + uint8_t abs_index = (index - this->offset); + if (abs_index >= this->message_length) { + return false; // out of bounds } - - value = (uint8_t)(((message_data[abs_index]) >> (bit)) & 0x01); + uint8_t val = value; + value = (uint8_t)(((this->message_data[abs_index]) >> (bit)) & 0x01); + if (val != value) { + return true; + } + return false; } - // read values from a telegram. We always store the value, regardless if its garbage + // read a value from a telegram. We always store the value, regardless if its garbage template // assuming negative numbers are stored as 2's-complement // https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c // 2-compliment : https://www.rapidtables.com/convert/number/decimal-to-hex.html // https://en.wikipedia.org/wiki/Two%27s_complement - // s is to override number of bytes read (e.g. use 3 to simulat a uint24_t) - void read_value(Value & value, const uint8_t index, uint8_t s = 0) const { - uint8_t size = (!s) ? sizeof(Value) : s; - int8_t abs_index = ((index - offset + size - 1) >= message_length - 1) ? -1 : (index - offset); - if (abs_index < 0) { - return; // out of bounds, we don't change the value + // s is to override number of bytes read (e.g. use 3 to simulate a uint24_t) + bool read_value(Value & value, const uint8_t index, uint8_t s = 0) const { + uint8_t num_bytes = (!s) ? sizeof(Value) : s; + // check for out of bounds, if so don't modify the value + if ((index < this->offset) || ((index - this->offset + num_bytes - 1) >= this->message_length)) { + return false; } - - value = 0; - for (uint8_t i = 0; i < size; i++) { - value = (value << 8) + message_data[abs_index + i]; // shift + auto val = value; + value = 0; + for (uint8_t i = 0; i < num_bytes; i++) { + value = (value << 8) + this->message_data[index - this->offset + i]; // shift by byte } + if (val != value) { + return true; + } + return false; } - private: int8_t _getDataPosition(const uint8_t index, const uint8_t size) const; }; @@ -182,13 +189,12 @@ class EMSbus { private: static constexpr uint32_t EMS_BUS_TIMEOUT = 30000; // timeout in ms before recognizing the ems bus is offline (30 seconds) - - static uint32_t last_bus_activity_; // timestamp of last time a valid Rx came in - static bool bus_connected_; // start assuming the bus hasn't been connected - static uint8_t ems_mask_; // unset=0xFF, buderus=0x00, junkers/ht3=0x80 - static uint8_t ems_bus_id_; // the bus id, which configurable and stored in settings - static uint8_t tx_mode_; // local copy of the tx mode - static uint8_t tx_state_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE) + static uint32_t last_bus_activity_; // timestamp of last time a valid Rx came in + static bool bus_connected_; // start assuming the bus hasn't been connected + static uint8_t ems_mask_; // unset=0xFF, buderus=0x00, junkers/ht3=0x80 + static uint8_t ems_bus_id_; // the bus id, which configurable and stored in settings + static uint8_t tx_mode_; // local copy of the tx mode + static uint8_t tx_state_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE) }; class RxService : public EMSbus { @@ -201,7 +207,7 @@ class RxService : public EMSbus { void loop(); void add(uint8_t * data, uint8_t length); - uint16_t telegram_count() const { + uint32_t telegram_count() const { return telegram_count_; } @@ -209,7 +215,7 @@ class RxService : public EMSbus { telegram_count_++; } - uint16_t telegram_error_count() const { + uint32_t telegram_error_count() const { return telegram_error_count_; } @@ -235,44 +241,38 @@ class RxService : public EMSbus { private: uint8_t rx_telegram_id_ = 0; // queue counter - uint16_t telegram_count_ = 0; // # Rx received - uint16_t telegram_error_count_ = 0; // # Rx CRC errors + uint32_t telegram_count_ = 0; // # Rx received + uint32_t telegram_error_count_ = 0; // # Rx CRC errors std::shared_ptr rx_telegram; // the incoming Rx telegram - - std::list rx_telegrams_; // the Rx Queue + std::list rx_telegrams_; // the Rx Queue }; class TxService : public EMSbus { public: - static constexpr size_t MAX_TX_TELEGRAMS = 20; // size of Tx queue - - static constexpr uint8_t TX_WRITE_FAIL = 4; // EMS return code for fail - static constexpr uint8_t TX_WRITE_SUCCESS = 1; // EMS return code for success + static constexpr size_t MAX_TX_TELEGRAMS = 20; // size of Tx queue + static constexpr uint8_t TX_WRITE_FAIL = 4; // EMS return code for fail + static constexpr uint8_t TX_WRITE_SUCCESS = 1; // EMS return code for success TxService() = default; ~TxService() = default; - void start(); - void send(); - - void add(const uint8_t operation, - const uint8_t dest, - const uint16_t type_id, - const uint8_t offset, - uint8_t * message_data, - const uint8_t message_length, - const bool front = false); - void add(const uint8_t operation, const uint8_t * data, const uint8_t length, const bool front = false); - - void read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset = 0); - - void send_raw(const char * telegram_data); - - void send_poll(); - - void flush_tx_queue(); - - void retry_tx(const uint8_t operation, const uint8_t * data, const uint8_t length); + void start(); + void send(); + void add(const uint8_t operation, + const uint8_t dest, + const uint16_t type_id, + const uint8_t offset, + uint8_t * message_data, + const uint8_t message_length, + const bool front = false); + void add(const uint8_t operation, const uint8_t * data, const uint8_t length, const bool front = false); + void read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset = 0); + void send_raw(const char * telegram_data); + void send_poll(); + void flush_tx_queue(); + void retry_tx(const uint8_t operation, const uint8_t * data, const uint8_t length); + bool is_last_tx(const uint8_t src, const uint8_t dest) const; + uint16_t post_send_query(); uint8_t retry_count() const { return retry_count_; @@ -282,13 +282,15 @@ class TxService : public EMSbus { retry_count_ = 0; } - bool is_last_tx(const uint8_t src, const uint8_t dest) const; - void set_post_send_query(uint16_t type_id) { telegram_last_post_send_query_ = type_id; } - uint16_t telegram_read_count() const { + uint16_t get_post_send_query() { + return telegram_last_post_send_query_; + } + + uint32_t telegram_read_count() const { return telegram_read_count_; } @@ -300,7 +302,7 @@ class TxService : public EMSbus { telegram_read_count_++; } - uint16_t telegram_fail_count() const { + uint32_t telegram_fail_count() const { return telegram_fail_count_; } @@ -312,7 +314,7 @@ class TxService : public EMSbus { telegram_fail_count_++; } - uint16_t telegram_write_count() const { + uint32_t telegram_write_count() const { return telegram_write_count_; } @@ -324,10 +326,6 @@ class TxService : public EMSbus { telegram_write_count_++; } - void post_send_query(); - - void print_last_tx(); - class QueuedTxTelegram { public: const uint16_t id_; @@ -355,9 +353,9 @@ class TxService : public EMSbus { private: std::list tx_telegrams_; // the Tx queue - uint16_t telegram_read_count_ = 0; // # Tx successful reads - uint16_t telegram_write_count_ = 0; // # Tx successful writes - uint16_t telegram_fail_count_ = 0; // # Tx unsuccessful transmits + uint32_t telegram_read_count_ = 0; // # Tx successful reads + uint32_t telegram_write_count_ = 0; // # Tx successful writes + uint32_t telegram_fail_count_ = 0; // # Tx unsuccessful transmits std::shared_ptr telegram_last_; uint16_t telegram_last_post_send_query_; // which type ID to query after a successful send, to read back the values just written diff --git a/src/test/test.cpp b/src/test/test.cpp index 524027744..0a6bee463 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -123,10 +123,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { uint8bitb = EMS_VALUE_UINT_NOTSET; telegram->read_bitvalue(uint8bitb, 0, 0); // value is 0x01 = 0000 0001 shell.printfln("uint8 bit read: expecting 1, got:%d", uint8bitb); - - shell.loop_all(); - - return; } if (command == "devices") { @@ -142,6 +138,9 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { // question: do we need to set the mask? std::string version("1.2.3"); EMSESP::add_device(0x08, 123, version, EMSdevice::Brand::BUDERUS); // Nefit Trendline + + // UBAuptime + uart_telegram({0x08, 0x0B, 0x14, 00, 0x3C, 0x1F, 0xAC, 0x70}); } // unknown device - @@ -159,7 +158,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { // note there is no brand (byte 9) rx_telegram({0x09, 0x0B, 0x02, 0x00, 0x59, 0x09, 0x0a}); - shell.loop_all(); EMSESP::show_device_values(shell); } @@ -210,8 +208,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { shell.invoke_command("show"); // shell.invoke_command("system"); // shell.invoke_command("show mqtt"); - - // shell.loop_all(); } if (command == "thermostat") { @@ -230,13 +226,34 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { // EMSESP::add_device(0x08, 123, version, EMSdevice::Brand::BUDERUS); // Nefit Trendline // add a thermostat - EMSESP::add_device(0x18, 157, version, EMSdevice::Brand::BOSCH); // Bosch CR100 - https://github.com/proddy/EMS-ESP/issues/355 + EMSESP::add_device(0x10, 192, version, EMSdevice::Brand::JUNKERS); // FW120 - // RCPLUSStatusMessage_HC1(0x01A5) - uart_telegram({0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24, + // HC1 + uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x6F, 0x00, 0xCF, 0x21, 0x2E, 0x20, 0x00, 0x2E, 0x24, 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); - shell.loop_all(); + // HC2 + uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x70, 0x00, 0xCF, 0x22, 0x2F, 0x10, 0x00, 0x2E, 0x24, + 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); + + // HC3 + uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + } + + if (command == "tc100") { + shell.printfln(F("Testing adding a TC100 thermostat to the EMS bus...")); + + std::string version("02.21"); + + // add a boiler + // EMSESP::add_device(0x08, 123, version, EMSdevice::Brand::BUDERUS); // Nefit Trendline + + // add a thermostat + EMSESP::add_device(0x18, 202, version, EMSdevice::Brand::BOSCH); // Bosch TC100 - https://github.com/proddy/EMS-ESP/issues/474 + + // 0x0A + uart_telegram({0x98, 0x0B, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); } if (command == "solar") { @@ -272,6 +289,39 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { EMSESP::show_device_values(shell); } + if (command == "solar200") { + shell.printfln(F("Testing Solar SM200")); + + EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS); + + std::string version("1.2.3"); + EMSESP::add_device(0x30, 164, version, EMSdevice::Brand::BUDERUS); // SM200 + + // SM100Monitor - type 0x0362 EMS+ - for SM100 and SM200 + // B0 0B FF 00 02 62 00 44 02 7A 80 00 80 00 80 00 80 00 80 00 80 00 00 7C 80 00 80 00 80 00 80 + rx_telegram({0xB0, 0x0B, 0xFF, 00, 0x02, 0x62, 00, 0x44, 0x02, 0x7A, 0x80, 00, 0x80, 0x00, 0x80, 00, + 0x80, 00, 0x80, 00, 0x80, 00, 00, 0x7C, 0x80, 00, 0x80, 00, 0x80, 00, 0x80}); + EMSESP::show_device_values(shell); + + rx_telegram({0xB0, 0x0B, 0xFF, 0x00, 0x02, 0x62, 0x01, 0x44, 0x03, 0x30, 0x80, 00, 0x80, 00, 0x80, 00, + 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 0x33}); + EMSESP::show_device_values(shell); + + rx_telegram({0xB0, 00, 0xFF, 0x18, 02, 0x62, 0x80, 00, 0xB8}); + EMSESP::show_device_values(shell); + + EMSESP::send_raw_telegram("B0 00 FF 18 02 62 80 00 B8"); + + uart_telegram("30 00 FF 0A 02 6A 04"); // SM100 pump on 1 + uart_telegram("30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 0B 09 64 00 00 00 00"); // SM100 modulation + + EMSESP::show_device_values(shell); + + uart_telegram("30 00 FF 0A 02 6A 03"); // SM100 pump off 0 + + EMSESP::show_device_values(shell); + } + if (command == "km") { shell.printfln(F("Testing KM200 Gateway")); @@ -353,7 +403,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); uart_telegram("98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03"); // without CRC - uart_telegram_withCRC("98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03 13"); // with CRC + uart_telegram_withCRC("98 00 FF 00 01 A6 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03 6B"); // with CRC EMSESP::txservice_.flush_tx_queue(); shell.loop_all(); @@ -524,8 +574,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { shell.invoke_command("help"); shell.invoke_command("pin"); shell.invoke_command("pin 1 true"); - - shell.loop_all(); } if (command == "mqtt") { @@ -539,7 +587,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { // add a thermostat EMSESP::add_device(0x18, 157, version, EMSdevice::Brand::BOSCH); // Bosch CR100 - https://github.com/proddy/EMS-ESP/issues/355 - // RCPLUSStatusMessage_HC1(0x01A5) + // RCPLUSStatusMessage_HC1(0x01A5) - HC1 uart_telegram({0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24, 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); uart_telegram("98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03"); // without CRC @@ -576,6 +624,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":1}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"id\":2}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":2}"); // hc as number + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":19.5,\"hc\":1}"); // data as number EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":\"2\"}"); // hc as string EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22.56}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22}"); @@ -596,8 +645,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { Mqtt::resubscribe(); Mqtt::show_mqtt(shell); // show queue - - shell.loop_all(); } if (command == "poll2") { @@ -656,7 +703,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { } // finally dump to console - shell.loop_all(); + EMSESP::loop(); } // simulates a telegram in the Rx queue, but without the CRC which is added automatically @@ -683,6 +730,7 @@ void Test::uart_telegram(const std::vector & rx_data) { } data[i] = EMSESP::rxservice_.calculate_crc(data, i); EMSESP::incoming_telegram(data, i + 1); + EMSESP::rxservice_.loop(); } // takes raw string, assuming it contains the CRC. This is what is output from 'watch raw' @@ -720,6 +768,7 @@ void Test::uart_telegram_withCRC(const char * rx_data) { } EMSESP::incoming_telegram(data, count + 1); + EMSESP::rxservice_.loop(); } // takes raw string, adds CRC to end @@ -759,6 +808,7 @@ void Test::uart_telegram(const char * rx_data) { data[count + 1] = EMSESP::rxservice_.calculate_crc(data, count + 1); // add CRC EMSESP::incoming_telegram(data, count + 2); + EMSESP::rxservice_.loop(); } #pragma GCC diagnostic push diff --git a/src/uart/emsuart_esp8266.cpp b/src/uart/emsuart_esp8266.cpp index 720a199bd..6dd63e365 100644 --- a/src/uart/emsuart_esp8266.cpp +++ b/src/uart/emsuart_esp8266.cpp @@ -46,21 +46,25 @@ void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) { if (USIS(EMSUART_UART) & ((1 << UIBD))) { // BREAK detection = End of EMS data block USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset tx-brk - if (emsTxBufIdx < emsTxBufLen) { // irq tx_mode is interrupted by - emsTxBufIdx = emsTxBufLen + 1; // stop tx - // drop_next_rx = true; // we have trash in buffer + if (sending_) { // irq tx_mode is interrupted by , should never happen + drop_next_rx = true; // we have trash in buffer } USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt length = 0; while ((USS(EMSUART_UART) >> USRXC) & 0x0FF) { // read fifo into buffer uint8_t rx = USF(EMSUART_UART); if (length < EMS_MAXBUFFERSIZE) { - uart_buffer[length++] = rx; + if (length || rx) { // skip a leading zero + uart_buffer[length++] = rx; + } } else { drop_next_rx = true; } } if (!drop_next_rx) { + if (uart_buffer[length - 1]) { // check if last byte is break + length++; + } pEMSRxBuf->length = length; os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity @@ -89,6 +93,9 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) { // ISR to Fire when Timer is triggered void ICACHE_RAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() { + if (!sending_) { + return; + } emsTxBufIdx++; if (emsTxBufIdx < emsTxBufLen) { USF(EMSUART_UART) = emsTxBuf[emsTxBufIdx]; diff --git a/src/version.h b/src/version.h index da21bc3c5..01450724a 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "2.0.0" +#define EMSESP_APP_VERSION "2.0.1"