v2.0.1 - Merge remote-tracking branch 'origin/dev' into main

This commit is contained in:
proddy
2020-09-13 11:20:40 +02:00
90 changed files with 4959 additions and 1150 deletions

View File

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

View File

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

10
.github/contribute.md vendored
View File

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

17
.github/stale.yml vendored
View File

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

View File

@@ -60,3 +60,4 @@ jobs:
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

1
.gitignore vendored
View File

@@ -16,7 +16,6 @@ pio_local.ini
# project specfic
/scripts/stackdmp.txt
*.bin
emsesp
/data/www/
/lib/framework/WWWData.h

View File

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

View File

@@ -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 <device ID> <type ID>` 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

110
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,110 @@
<img src="/media/EMS-ESP_logo_dark.png" alt="Logo" align="right" height="76"/>
# 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 isnt 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_ youre 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.
<Other>
<A table should be maintained for relating maintainers and components. When triaging, this is essential to figure out if someone in particular should be consulted about specific changes.>
<A stable release cadence should be established, e.g.: every month.>

281
README.md
View File

@@ -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)
<br />
**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)
<br />
[![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")
<br/>
[![gitter](https://img.shields.io/gitter/room/EMS-ESP/EMS-ESP.svg)](https://gitter.im/EMS-ESP/community)
<br>
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/.
<img src="media/gateway-integration.jpg" width=40%>
---
## **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
<img src="media/web_settings.PNG" width=70% height=70%>
<img src="media/web_status.PNG" width=70% height=70%>
<img src="media/web_devices.PNG" width=70% height=70%>
<img src="media/web_mqtt.PNG" width=70% height=70%>
- 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**
| | |
| --- | --- |
| <img src="media/web_settings.PNG"> | <img src="media/web_status.PNG"> |
| <img src="media/web_devices.PNG"> | <img src="media/web_mqtt.PNG"> |
<img src="media/console.PNG" width=100% height=100%>
## **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 = <COM>
```
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 <firmware.bin>`
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 <COM PORT> -b 921600 write_flash 0x00000 <firmware.bin>`
For ESP8266: `esptool.py -p <COM PORT> -b 921600 write_flash 0x00000 <firmware.bin>`
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 <firmware.bin>`
- Uploading over WiFi: `espota.py --debug --progress --port 8266 --auth ems-esp-neo -i <IP address> -f <firmware.bin>`
`espota.py --debug --progress --port 8266 --auth ems-esp-neo -i <IP address> -f <firmware.bin>`
## **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 <on | off | raw> [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 <device ID> *
set tx_mode <n> *
show
show devices
show ems
show values
show mqtt
system
set
show
format *
show users *
passwd *
restart *
set wifi hostname <name> *
set wifi password *
set wifi ssid <name> *
wifi reconnect *
pin <gpio> [data] *
boiler
read <type ID> *
call [cmd] [data] *
thermostat
set
set master [device ID] *
read <type ID> *
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
* `<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 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":<cmd> ,"data":<data>, "id":<n>}`.
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 <hot, eco, intelligent>
flowtemp <degrees>
wwtemp <degrees>
boilhyston <degrees> (negative value)
boilhystoff <degrees> (positive value)
burnperiod <minutes>
burnminpower <%>
burnmaxpower <%>
pumpdelay <minutes>
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 <off | on | auto>
calinttemp <degrees>
minexttemp <degrees>
building <light | medium | heavy>
language <n> (0=de, 1=nl, 2=fr, 3=it) only RC30
display <n> (0=int temp, 1= int set, 2=ext. temp, 3=burner, 4=ww, 5=mode, 6=time, 7=date, 8=smoke) only RC30
clockoffset <seconds> (only RC30)
--- with hc ---
mode <auto | night | day | nofrost | heat | eco>
temp <degrees>
nighttemp <degrees>
daytemp <degrees>
nofrosttemp <degrees>
ecotemp <degrees>
heattemp <degrees>
summertemp <degrees>
designtemp <degrees>
offsettemp <degrees>
holidaytemp <degrees>
remotetemp <degrees>
control <0 | 1 | 2>
pause <hours>
party <hours>
holiday <dd.mm.yyyy-dd.mm.yyyy>
date <NTP | hh:mm:ss-dd.mm.yyyy-dw-dst>
If you're looking for support on **EMS-ESP** there are some options available:
*system_cmd*
send <"0B XX XX ..">
pin <gpio> <on|off|1|0|true|false>
### 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

55
doc/MQTT.md Normal file
View File

@@ -0,0 +1,55 @@
# **MQTT commands**
All commands must be written as `{"cmd":<cmd> ,"data":<data>, "id":<n>}`.
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 <hot, eco, intelligent>
flowtemp <degrees>
wwtemp <degrees>
boilhyston <degrees> (negative value)
boilhystoff <degrees> (positive value)
burnperiod <minutes>
burnminpower <%>
burnmaxpower <%>
pumpdelay <minutes>
```
topic = *thermostat_cmd*
```
--- without hc ---
wwmode <off | on | auto>
calinttemp <degrees>
minexttemp <degrees>
building <light | medium | heavy>
language <n> (0=de, 1=nl, 2=fr, 3=it) only RC30
display <n> (0=int temp, 1= int set, 2=ext. temp, 3=burner, 4=ww, 5=mode, 6=time, 7=date, 8=smoke) only RC30
clockoffset <seconds> (only RC30)
--- with hc ---
mode <auto | night | day | nofrost | heat | eco>
temp <degrees>
nighttemp <degrees>
daytemp <degrees>
nofrosttemp <degrees>
ecotemp <degrees>
heattemp <degrees>
summertemp <degrees>
designtemp <degrees>
offsettemp <degrees>
holidaytemp <degrees>
remotetemp <degrees>
control <0 | 1 | 2>
pause <hours>
party <hours>
holiday <dd.mm.yyyy-dd.mm.yyyy>
date <NTP | hh:mm:ss-dd.mm.yyyy-dw-dst>
```
topic = *system_cmd*
```
send <"0B XX XX ..">
pin <gpio> <on|off|1|0|true|false>
```

View File

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

72
doc/console.md Normal file
View File

@@ -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
* `<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 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 <on | off | raw> [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 <device ID> *
set tx_mode <n> *
show
show devices
show ems
show values
show mqtt
read <device ID> <type ID> *
system
set
show
format *
show users *
passwd *
restart *
set wifi hostname <name> *
set wifi password *
set wifi ssid <name> *
wifi reconnect *
pin <gpio> [data] *
boiler
read <type ID> *
call [cmd] [data] *
thermostat
set
set master [device ID] *
read <type ID> *
call [cmd] [data] [heating circuit] *
```

View File

@@ -150,15 +150,75 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
<MenuItem value={2}>2</MenuItem>
</SelectValidator>
<TextValidator
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
name="publish_time"
label="MQTT Publish Time (seconds)"
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
name="publish_time_boiler"
label="MQTT Boiler Publish Period (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time}
value={data.publish_time_boiler}
type="number"
onChange={handleValueChange('publish_time')}
onChange={handleValueChange('publish_time_boiler')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
name="publish_time_thermostat"
label="MQTT Thermostat Publish Period (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_thermostat}
type="number"
onChange={handleValueChange('publish_time_thermostat')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
name="publish_time_solar"
label="MQTT Solar Publish Period (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_solar}
type="number"
onChange={handleValueChange('publish_time_solar')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
name="publish_time_mixing"
label="MQTT Mixer Publish Period (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_mixing}
type="number"
onChange={handleValueChange('publish_time_mixing')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
name="publish_time_other"
label="MQTT other Modules Publish Period (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_other}
type="number"
onChange={handleValueChange('publish_time_other')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
name="publish_time_sensor"
label="MQTT Sensors Publish Period (seconds, 0=on change)"
fullWidth
variant="outlined"
value={data.publish_time_sensor}
type="number"
onChange={handleValueChange('publish_time_sensor')}
margin="normal"
/>
<FormActions>

View File

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

View File

@@ -83,8 +83,11 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
const { width, data } = this.props;
return (
<TableContainer>
<Typography variant="h6" color="primary" paragraph>
Devices:
<Typography variant="h6" color="primary" >
Devices
</Typography>
<Typography variant="caption" color="initial" paragraph>
<i>(click to show details)</i>
</Typography>
{!this.noDevices() && (
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
@@ -145,14 +148,14 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
<TableContainer>
<p></p>
<Typography variant="h6" color="primary" paragraph>
Sensors:
Sensors
</Typography>
{!this.noSensors() && (
<Table size="small" padding="default">
<TableHead>
<TableRow>
<StyledTableCell>ID</StyledTableCell>
<StyledTableCell align="left">Temperature</StyledTableCell>
<StyledTableCell align="right">Temperature</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -161,8 +164,8 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
<TableCell component="th" scope="row">
{sensorData.id}
</TableCell>
<TableCell align="left">
{sensorData.temp}&deg;C
<TableCell align="right">
{sensorData.temp.toFixed(1)}&deg;C
</TableCell>
</TableRow>
))}
@@ -285,7 +288,7 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
<TableCell component="th" scope="row">
{deviceData.name}
</TableCell>
<TableCell align="left">
<TableCell align="right">
{deviceData.value}
</TableCell>
</TableRow>

View File

@@ -10,14 +10,14 @@ class EMSESPHelp extends Component {
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
<Typography variant="body1">
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.
<p></p>
Please consider supporting this project via the GitHub page <Link href="https://github.com/proddy/EMS-ESP" color="primary">{'http://github.com/proddy/EMS-ESP'}</Link>.
</Typography>
</Box>
<br></br>
<Typography variant="body1" paragraph>
Check for news and updates on the <Link href="https://emsesp.github.io/docs/#/" color="primary">{'Wiki'}</Link>.
Check for news and updates on the <Link href="https://bbqkees-electronics.nl/wiki/" color="primary">{'Wiki'}</Link>.
</Typography>
<Typography variant="body1" paragraph>
For live community chat go to <Link href="https://gitter.im/EMS-ESP/community#" color="primary">{'Gitter'}</Link>.

View File

@@ -48,9 +48,13 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps)
<ValidatorForm onSubmit={saveData}>
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
<Typography variant="body1">
Customize EMS-ESP by editing the default settings here. Refer to the <Link href="https://emsesp.github.io/docs/#/" color="primary">{'Wiki'}</Link>&nbsp;for descriptions of each setting.
Customize EMS-ESP by modifying the default settings here.
</Typography>
</Box>
<br></br>
<Typography variant="h6" color="primary" >
EMS Bus Settings
</Typography>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:255']}
errorMessages={['TX mode is required', "Must be a number", "Must be 0 or higher", "Max value is 255"]}
@@ -100,18 +104,10 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps)
onChange={handleValueChange('tx_gpio')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['LED GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 255"]}
name="led_gpio"
label="LED GPIO pin (0=none)"
fullWidth
variant="outlined"
value={data.led_gpio}
type="number"
onChange={handleValueChange('led_gpio')}
margin="normal"
/>
<br></br>
<Typography variant="h6" color="primary" >
Dallas Sensor Settings
</Typography>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['Dallas GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 255"]}
@@ -124,6 +120,32 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps)
onChange={handleValueChange('dallas_gpio')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.dallas_parasite}
onChange={handleValueChange('dallas_parasite')}
value="dallas_parasite"
/>
}
label="Dallas Parasite Mode"
/>
<br></br>
<Typography variant="h6" color="primary" >
LED Settings
</Typography>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
errorMessages={['LED GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 255"]}
name="led_gpio"
label="LED GPIO pin (0=none)"
fullWidth
variant="outlined"
value={data.led_gpio}
type="number"
onChange={handleValueChange('led_gpio')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
@@ -132,8 +154,12 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps)
value="hide_led"
/>
}
label="Hide LED"
label="Invert/Hide LED"
/>
<br></br>
<Typography variant="h6" color="primary" >
Shower Settings
</Typography>
<BlockFormControlLabel
control={
<Checkbox
@@ -154,6 +180,21 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps)
}
label="Shower Alert"
/>
<br></br>
<Typography variant="h6" color="primary" >
Syslog Settings
</Typography>
<TextValidator
validators={['isIPOrHostname']}
errorMessages={["Not a valid IP address or hostname"]}
name="syslog_host"
label="Syslog IP/Host"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={handleValueChange('syslog_host')}
margin="normal"
/>
<SelectValidator name="syslog_level"
label="Syslog Log Level"
value={data.syslog_level}
@@ -166,33 +207,18 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps)
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
</SelectValidator>
{data.syslog_level !== -1 &&
<Fragment>
<TextValidator
validators={['isIPOrHostname']}
errorMessages={["Not a valid IP address or hostname"]}
name="syslog_host"
label="Syslog IP/Host"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={handleValueChange('syslog_host')}
margin="normal"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Syslog Mark is required', "Must be a number", "Must be 0 or higher (0=off)", "Max value is 65535"]}
name="syslog_mark_interval"
label="Syslog Mark Interval (seconds, 0=off)"
fullWidth
variant="outlined"
value={data.syslog_mark_interval}
type="number"
onChange={handleValueChange('syslog_mark_interval')}
margin="normal"
/>
</Fragment>
}
<TextValidator
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
errorMessages={['Syslog Mark is required', "Must be a number", "Must be 0 or higher (0=off)", "Max value is 65535"]}
name="syslog_mark_interval"
label="Syslog Mark Interval (seconds, 0=off)"
fullWidth
variant="outlined"
value={data.syslog_mark_interval}
type="number"
onChange={handleValueChange('syslog_mark_interval')}
margin="normal"
/>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
Save

View File

@@ -37,6 +37,10 @@ import {
import { EMSESPStatus } from "./EMSESPtypes";
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> & WithTheme & WithWidthProps;
const StyledTableCell = withStyles((theme: Theme) =>
@@ -75,7 +79,7 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
<TableHead>
<TableRow>
<StyledTableCell>Statistic</StyledTableCell>
<StyledTableCell align="center"># Telegrams</StyledTableCell>
<StyledTableCell align="right"># Telegrams</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -83,25 +87,25 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
<TableCell>
(Rx) Received telegrams
</TableCell>
<TableCell align="center">{data.rx_received}</TableCell>
<TableCell align="right">{formatNumber(data.rx_received)}</TableCell>
</TableRow>
<TableRow>
<TableCell >
(Rx) Incomplete telegrams
</TableCell>
<TableCell align="center">{data.crc_errors}</TableCell>
<TableCell align="right">{formatNumber(data.crc_errors)}</TableCell>
</TableRow>
<TableRow>
<TableCell >
(Tx) Successfully sent telegrams
</TableCell>
<TableCell align="center">{data.tx_sent}</TableCell>
<TableCell align="right">{formatNumber(data.tx_sent)}</TableCell>
</TableRow>
<TableRow>
<TableCell >
(Tx) Send Errors
</TableCell>
<TableCell align="center">{data.tx_errors}</TableCell>
<TableCell align="right">{formatNumber(data.tx_errors)}</TableCell>
</TableRow>
</TableBody>
</Table>

View File

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

View File

@@ -2,23 +2,29 @@
#define SPIFFSEditor_H_
#include <ESPAsyncWebServer.h>
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

View File

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

View File

@@ -7,6 +7,8 @@
#include <DNSServer.h>
#include <IPAddress.h>
#include <uuid/common.h>
#define MANAGE_NETWORK_DELAY 10000

View File

@@ -31,6 +31,12 @@ class FSPersistence {
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
// 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);

View File

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

View File

@@ -6,12 +6,13 @@
#include <FSPersistence.h>
#include <AsyncMqttClient.h>
#include <ESPUtils.h>
#include <uuid/common.h>
#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;

View File

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

View File

@@ -16,6 +16,8 @@
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <uuid/common.h>
#define MAX_NTP_STATUS_SIZE 1024
#define NTP_STATUS_SERVICE_PATH "/rest/ntpStatus"

View File

@@ -8,7 +8,7 @@
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <FS.h>
// #include <FS.h>
#include <LittleFS.h> // proddy added
#endif

View File

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

View File

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

BIN
media/console.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

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

0
scripts/analyze_stackdmp.py Normal file → Executable file
View File

BIN
scripts/boot_app0.bin Normal file

Binary file not shown.

Binary file not shown.

0
scripts/build_interface.py Normal file → Executable file
View File

0
scripts/clean_fw.py Normal file → Executable file
View File

0
scripts/decoder.py Normal file → Executable file
View File

0
scripts/decoder_linux.py Normal file → Executable file
View File

0
scripts/espota.py Normal file → Executable file
View File

2959
scripts/esptool.py Executable file

File diff suppressed because it is too large Load Diff

0
scripts/main_script.py Normal file → Executable file
View File

0
scripts/memanalyzer.py Normal file → Executable file
View File

View File

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

BIN
scripts/partitions.bin Normal file

Binary file not shown.

15
scripts/upload_esp32.py Executable file
View File

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

View File

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

View File

@@ -24,7 +24,7 @@
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#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"

View File

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

View File

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

View File

@@ -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<std::string> & 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<std::string> & 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

View File

@@ -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<const Telegram> t) { process_UBAMaintenanceStatus(t); });
register_telegram_type(0x2A, F("MC10Status"), false, [&](std::shared_ptr<const Telegram> t) { process_MC10Status(t); });
register_telegram_type(0x33, F("UBAParameterWW"), true, [&](std::shared_ptr<const Telegram> t) { process_UBAParameterWW(t); });
register_telegram_type(0x14, F("UBATotalUptime"), false, [&](std::shared_ptr<const Telegram> t) { process_UBATotalUptime(t); });
register_telegram_type(0x14, F("UBATotalUptime"), true, [&](std::shared_ptr<const Telegram> t) { process_UBATotalUptime(t); });
register_telegram_type(0x35, F("UBAFlags"), false, [&](std::shared_ptr<const Telegram> t) { process_UBAFlags(t); });
register_telegram_type(0x15, F("UBAMaintenanceData"), false, [&](std::shared_ptr<const Telegram> t) { process_UBAMaintenanceData(t); });
register_telegram_type(0x16, F("UBAParameters"), true, [&](std::shared_ptr<const Telegram> 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<EMSESP_MAX_JSON_SIZE_MEDIUM> 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<const Telegram> 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<const Telegram> 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<const Telegram> telegram) {
* received only after requested (not broadcasted)
*/
void Boiler::process_UBATotalUptime(std::shared_ptr<const Telegram> 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<const Telegram> 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<const Telegram> telegram) {
* received every 10 seconds
*/
void Boiler::process_UBAMonitorWW(std::shared_ptr<const Telegram> 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<const Telegram> telegram) {
* Still to figure out are: serviceCode, retTemp, sysPress
*/
void Boiler::process_UBAMonitorFastPlus(std::shared_ptr<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> telegram) {
telegram->read_value(extTemp_, 0);
changed_ |= telegram->read_value(extTemp_, 0);
}
// UBASetPoint 0x1A
void Boiler::process_UBASetPoints(std::shared_ptr<const Telegram> 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

View File

@@ -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<const Telegram> telegram);
void process_UBAMonitorSlowPlus(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorSlowPlus2(std::shared_ptr<const Telegram> telegram);
void process_UBAOutdoorTemp(std::shared_ptr<const Telegram> telegram);
void process_UBASetPoints(std::shared_ptr<const Telegram> telegram);
void process_UBAFlags(std::shared_ptr<const Telegram> telegram);
@@ -155,11 +158,8 @@ class Boiler : public EMSdevice {
void process_UBAMaintenanceStatus(std::shared_ptr<const Telegram> telegram);
void process_UBAMaintenanceData(std::shared_ptr<const Telegram> telegram);
void process_UBAErrorMessage(std::shared_ptr<const Telegram> telegram);
void process_UBADHWStatus(std::shared_ptr<const Telegram> 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<EMSESP_MAX_JSON_SIZE_SMALL> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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

View File

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

View File

@@ -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<const Telegram> 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<const Telegram> telegram) {
* bytes 20+21 = TS6 Temperature sensor external heat exchanger
*/
void Solar::process_SM100Monitor(std::shared_ptr<const Telegram> 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<const Telegram> telegram) {
// SM100Config - 0x0366
// e.g. B0 00 FF 00 02 66 01 62 00 13 40 14
void Solar::process_SM100Config(std::shared_ptr<const Telegram> 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<const Telegram> telegram) {
void Solar::process_SM100Status(std::shared_ptr<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> telegram) {
telegram->read_value(setpoint_maxBottomTemp_, 6);
changed_ |= telegram->read_value(setpoint_maxBottomTemp_, 6);
}
} // namespace emsesp

View File

@@ -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<const Telegram> telegram);
void process_SM100Monitor(std::shared_ptr<const Telegram> telegram);

View File

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

View File

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

View File

@@ -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<const Telegram> 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<const Telegram> 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<const Telegram> t) { process_RC20Monitor(t); });
register_telegram_type(set_typeids[i], F("RC20Set"), false, [&](std::shared_ptr<const Telegram> 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<const Telegram> 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<const Telegram> t) { process_RC20Monitor_2(t); });
register_telegram_type(set_typeids[i], F("RC20Set"), false, [&](std::shared_ptr<const Telegram> 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<const Telegram> 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<const Telegram> t) { process_EasyMonitor(t); });
register_telegram_type(monitor_typeids[0], F("EasyMonitor"), true, [&](std::shared_ptr<const Telegram> 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<const Telegram> 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<const Telegram> 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<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
JsonObject rootThermostat = doc.to<JsonObject>();
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<JsonObject>(); // 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<n>
// 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<n>
// 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<JsonObject>(); // 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<JsonObject>(); // 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::HeatingCircuit> 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::HeatingCircuit> Thermostat::heating_circuit(std::sha
}
// create a new heating circuit object
auto new_hc = std::make_shared<Thermostat::HeatingCircuit>(hc_num, monitor_typeids[hc_num - 1], set_typeids[hc_num - 1]);
auto new_hc = std::make_shared<Thermostat::HeatingCircuit>(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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram)
void Thermostat::process_RC20Set_2(std::shared_ptr<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
// type 0x0165, ff
void Thermostat::process_JunkersSet(std::shared_ptr<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> 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<const Telegram> 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<const Telegram> telegram
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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<const Telegram> telegram) {
void Thermostat::process_RC30Monitor(std::shared_ptr<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> 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<const Telegram> 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<const Telegram> 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<std::string> & 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); });

View File

@@ -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<HeatingCircuit> & lhs, const std::shared_ptr<HeatingCircuit> & 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<uint16_t> monitor_typeids;
std::vector<uint16_t> 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

View File

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

View File

@@ -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<const Telegram> telegram);

View File

@@ -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<const Telegram> telegram) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> 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<const Telegram> 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<const Telegram> 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<const Telegram> 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
}
}

View File

@@ -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<const Telegram> telegram);
static void process_version(std::shared_ptr<const Telegram> telegram);
static void publish_response(std::shared_ptr<const Telegram> 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_;

View File

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

View File

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

View File

@@ -108,7 +108,6 @@ MAKE_PSTR(gpio_mandatory, "<gpio>")
MAKE_PSTR(data_optional, "[data]")
MAKE_PSTR(typeid_mandatory, "<type ID>")
MAKE_PSTR(deviceid_mandatory, "<device ID>")
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]")

View File

@@ -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::MQTTSubFunction> Mqtt::mqtt_subfunctions_;
std::vector<Mqtt::MQTTCmdFunction> 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() {

View File

@@ -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<MQTTSubFunction> mqtt_subfunctions_; // list of mqtt subscribe callbacks for all devices
static std::vector<MQTTCmdFunction> 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

View File

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

View File

@@ -60,6 +60,7 @@ class Sensors {
void loop();
void publish_values();
void reload();
bool updated_values();
const std::vector<Device> 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <typename Value>
// 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<const Telegram> rx_telegram; // the incoming Rx telegram
std::list<QueuedRxTelegram> rx_telegrams_; // the Rx Queue
std::list<QueuedRxTelegram> 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<QueuedTxTelegram> 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> 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

View File

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

View File

@@ -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 <brk>
emsTxBufIdx = emsTxBufLen + 1; // stop tx
// drop_next_rx = true; // we have trash in buffer
if (sending_) { // irq tx_mode is interrupted by <brk>, 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];

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "2.0.0"
#define EMSESP_APP_VERSION "2.0.1"