Browse Source

Merge remote-tracking branch 'hoseperez/dev' into kotel2

pull/2601/head
Dmitry Blinov 5 months ago
parent
commit
1b658abfcc
340 changed files with 70512 additions and 45289 deletions
  1. +0
    -3
      .bandit
  2. +2
    -0
      .gitattributes
  3. +90
    -0
      .github/ISSUE_TEMPLATE/bug-report.yml
  4. +0
    -67
      .github/ISSUE_TEMPLATE/bug_report.md
  5. +10
    -0
      .github/ISSUE_TEMPLATE/config.yml
  6. +35
    -0
      .github/ISSUE_TEMPLATE/feature-request.yml
  7. +0
    -26
      .github/ISSUE_TEMPLATE/feature_request.md
  8. +0
    -42
      .github/ISSUE_TEMPLATE/questions---troubleshooting.md
  9. +41
    -0
      .github/ISSUE_TEMPLATE/questions.yml
  10. +11
    -0
      .github/release_template.md
  11. +47
    -32
      .github/workflows/push.yml
  12. +146
    -0
      .github/workflows/release.yml
  13. +18
    -12
      .gitignore
  14. +369
    -200
      CHANGELOG.md
  15. +40
    -45
      README.md
  16. +1
    -2
      ci_install.sh
  17. +17
    -6
      ci_script.sh
  18. +0
    -14
      code/.gitignore
  19. +2
    -1
      code/.htmlvalidate.json
  20. +121
    -86
      code/espurna/alexa.cpp
  21. +0
    -2
      code/espurna/alexa.h
  22. +281
    -208
      code/espurna/api.cpp
  23. +28
    -21
      code/espurna/api.h
  24. +17
    -0
      code/espurna/api_async_server.h
  25. +102
    -33
      code/espurna/api_common.cpp
  26. +36
    -40
      code/espurna/api_impl.h
  27. +50
    -13
      code/espurna/api_path.h
  28. +0
    -405
      code/espurna/board.cpp
  29. +0
    -19
      code/espurna/board.h
  30. +478
    -0
      code/espurna/build.cpp
  31. +51
    -0
      code/espurna/build.h
  32. +1403
    -555
      code/espurna/button.cpp
  33. +8
    -54
      code/espurna/button.h
  34. +0
    -329
      code/espurna/button_config.h
  35. +42
    -19
      code/espurna/compat.h
  36. +1
    -2
      code/espurna/config/all.h
  37. +122
    -84
      code/espurna/config/arduino.h
  38. +0
    -97
      code/espurna/config/buildtime.h
  39. +34
    -30
      code/espurna/config/custom.h.example
  40. +289
    -28
      code/espurna/config/defaults.h
  41. +88
    -4
      code/espurna/config/dependencies.h
  42. +73
    -2
      code/espurna/config/deprecated.h
  43. +275
    -286
      code/espurna/config/general.h
  44. +602
    -204
      code/espurna/config/hardware.h
  45. +231
    -133
      code/espurna/config/sensors.h
  46. +80
    -56
      code/espurna/config/types.h
  47. +1
    -1
      code/espurna/config/version.h
  48. +182
    -89
      code/espurna/crash.cpp
  49. +0
    -39
      code/espurna/crash.h
  50. +26
    -32
      code/espurna/curtain_kingart.cpp
  51. +1
    -3
      code/espurna/curtain_kingart.h
  52. BIN
      code/espurna/data/index.all.html.gz
  53. BIN
      code/espurna/data/index.curtain.html.gz
  54. BIN
      code/espurna/data/index.garland.html.gz
  55. BIN
      code/espurna/data/index.light.html.gz
  56. BIN
      code/espurna/data/index.lightfox.html.gz
  57. BIN
      code/espurna/data/index.rfbridge.html.gz
  58. BIN
      code/espurna/data/index.rfm69.html.gz
  59. BIN
      code/espurna/data/index.sensor.html.gz
  60. BIN
      code/espurna/data/index.small.html.gz
  61. BIN
      code/espurna/data/index.thermostat.html.gz
  62. +585
    -300
      code/espurna/debug.cpp
  63. +2
    -6
      code/espurna/debug.h
  64. +419
    -180
      code/espurna/domoticz.cpp
  65. +3
    -17
      code/espurna/domoticz.h
  66. +0
    -4
      code/espurna/dummy_ets_printf.c
  67. +3
    -1
      code/espurna/encoder.cpp
  68. +0
    -2
      code/espurna/encoder.h
  69. +27
    -22
      code/espurna/esp8266_pwm.c
  70. +14
    -7
      code/espurna/espurna.h
  71. +9
    -0
      code/espurna/fan.h
  72. +18
    -15
      code/espurna/filters/BaseFilter.h
  73. +16
    -29
      code/espurna/filters/LastFilter.h
  74. +14
    -26
      code/espurna/filters/MaxFilter.h
  75. +56
    -69
      code/espurna/filters/MedianFilter.h
  76. +28
    -40
      code/espurna/filters/MovingAverageFilter.h
  77. +15
    -25
      code/espurna/filters/SumFilter.h
  78. +167
    -164
      code/espurna/garland.cpp
  79. +3
    -6
      code/espurna/garland.h
  80. +4
    -3
      code/espurna/garland/anim.h
  81. +17
    -8
      code/espurna/garland/animations/anim_comets.h
  82. +20
    -12
      code/espurna/garland/animations/anim_dolphins.h
  83. +7
    -7
      code/espurna/garland/animations/anim_fountain.h
  84. +17
    -13
      code/espurna/garland/animations/anim_salut.h
  85. +0
    -1
      code/espurna/garland/animations/anim_sparkr.h
  86. +18
    -22
      code/espurna/garland/color.h
  87. +0
    -6
      code/espurna/garland/palette.h
  88. +16
    -25
      code/espurna/garland/scene.h
  89. +868
    -72
      code/espurna/gpio.cpp
  90. +71
    -26
      code/espurna/gpio.h
  91. +0
    -53
      code/espurna/gpio_pin.h
  92. +366
    -206
      code/espurna/homeassistant.cpp
  93. +0
    -2
      code/espurna/homeassistant.h
  94. +350
    -154
      code/espurna/i2c.cpp
  95. +13
    -9
      code/espurna/i2c.h
  96. +355
    -216
      code/espurna/ifan.cpp
  97. +44
    -29
      code/espurna/influxdb.cpp
  98. +0
    -2
      code/espurna/influxdb.h
  99. +2051
    -354
      code/espurna/ir.cpp
  100. +0
    -2
      code/espurna/ir.h

+ 0
- 3
.bandit View File

@ -1,3 +0,0 @@
# Ignore B404 "Avoid importing subprocess"
# Ignore B603 "Subprocess without shell equals true"
skips: ['B404', 'B603']

+ 2
- 0
.gitattributes View File

@ -3,4 +3,6 @@
*.ini text eol=lf
*.h text eol=lf
*.cpp text eol=lf
*.re text eol=lf
*.ipp text eol=lf
*.c text eol=lf

+ 90
- 0
.github/ISSUE_TEMPLATE/bug-report.yml View File

@ -0,0 +1,90 @@
name: Bug report
description: Report any problem here
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
* Please search through existing [issues](https://github.com/xoseperez/espurna/issues) (both open and closed) before reporting a problem
* Check out our [wiki](https://github.com/xoseperez/espurna/wiki) for any common questions and or issues
* Consider asking at our [Gitter chat](https://gitter.im/tinkerman-cat/espurna) ([which is also accessible with any Matrix client!](https://matrix.to/#/#tinkerman-cat_espurna:gitter.im))
* Re-check our existing issues yet again :)
- type: input
id: device
attributes:
label: Device
description: Used development board or device brand, model and version.
placeholder: Sonoff Mini
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: |
ESPurna version. Can be either
* .bin filename or snapshot name / date, if downloaded from 'Releases'
* 'Firmware version' in WebUI
* First line of `info` terminal command output
* Output of `git rev-parse HEAD`, in the directory that was created when you cloned this repository
placeholder: 1.16.0-dev.git12345678+github250718
validations:
required: true
- type: textarea
attributes:
label: Bug description
description: A concise description of what the bug is and what is the expected behaviour (if any)
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior
- type: textarea
attributes:
label: Build tools used
description: |
If ESPurna was built manually, please describe build configuration and the tools that were used.
Please mention **any modifications** to our configuration headers or command line flags.
For ArduinoIDE / arduino-cli, which version was used.
For PlatformIO, also include
* What environment was used *or* what command line was used to build
* Output of `pio system info`
- type: textarea
attributes:
label: Any relevant log output (when available)
render: shell
description: |
Please copy and paste any relevant log output. It will be automatically formatted into code, so no need for backticks.
- type: textarea
attributes:
label: Decoded stack trace (when available)
render: markdown
description: |
Please provide an output of a stack decoder if this bug results in a crash
* See serial output, if device is connected to the computer
* While using WebUI, use DEBUG pannel and call `crash` command
* Call `crash` while connected to the telnet socket
Note that no source code changes or rebuilds should happen in the meantime. Decoder output would not be helpful unless it is the **exact same version that was uploaded to the device**!
* When telnet connection is available, run `pio device monitor --echo --port socket://<DEVICE IP OR HOSTNAME>:23 --environment <ENV> --filter esp8266_exception_decoder` and call `crash`
* When connected via serial, run `pio device monitor --echo --environment <ENV> --filter esp8266_exception_decoder` and call `crash`. Or, wait for the crash to happen.
* https://github.com/xoseperez/espurna/blob/dev/code/scripts/decoder.py (when using PlatformIO or Arduino IDE, either using the intermediate .elf file from the build directory; or, when using releases, similarly named .elf.debug from the Debug.zip)
* https://github.com/me-no-dev/EspExceptionDecoder (when using Arduino IDE)
placeholder: |
Abort called
sp: 3ffffde0 end: 3fffffc0 offset: 0000
ctx: cont
...
validations:
required: false

+ 0
- 67
.github/ISSUE_TEMPLATE/bug_report.md View File

@ -1,67 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
*Before creating a new issue please check that you have:*
* *searched the existing [issues](https://github.com/xoseperez/espurna/issues) (both open and closed)*
* *searched the [wiki](https://github.com/xoseperez/espurna/wiki)*
* *asked for help in the [chat](https://gitter.im/tinkerman-cat/espurna)*
* *done the previous things again :)*
*Fulfilling 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.*
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots or crash report) then you can delete them.*
**Bug description**
*A clear and concise description of what the bug is.*
**Steps to reproduce**
*Steps to reproduce the behavior.*
**Expected behavior**
*A clear and concise description of what you expected to happen.*
**Screenshots**
*If applicable, add screenshots to help explain your problem.*
**Device information**
*Copy-paste here the information as it is outputted by the device. You can get this information by typing `info` via serial, terminal or in the debug tab in the web UI. The relevant information is that surrounded by the scissors-cut lines (`---8<-------`).*
*If you cannot get this info from the device, please answer this questions:*
* *Arduino Core version*
* *ESPurna version*
* *Flash mode*
* *Device brand, model and version*
**Crash report**
*Save the crash postmortem message, which may look something like:*
> Exception (28):
> epc1=0x4021e698 epc2=0x00000000 epc3=0x00000000 excvaddr=0x00000000 depc=0x00000000
>
> ctx: sys
> sp: 3ffffc90 end: 3fffffb0 offset: 01a0
> \>\>\>stack\>\>\>
> ...
> \<\<\<stack\<\<\<
>
*And, use one of the following tools to decode it:*
- *https://github.com/mcspr/EspArduinoExceptionDecoder*
- *https://github.com/me-no-dev/EspExceptionDecoder (when using Arduino IDE)*
*When using PlatformIO, it is also possible to use a built-in exception decoder when device's serial connection is available:*
- `pio device monitor --echo -e $environment -f esp8266_exception_decoder`
**Tools used**
* *Desktop operating system*
* *Browser & version*
* *IDE & version*
* *Compiler & version (if not embedded in IDE)*
**Additional context**
*Add any other context about the problem here.*

+ 10
- 0
.github/ISSUE_TEMPLATE/config.yml View File

@ -0,0 +1,10 @@
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: true
contact_links:
- name: Gitter chat
url: https://gitter.im/tinkerman-cat/espurna
about: ESPurna chat room
- name: Matrix chat
url: https://matrix.to/#/#tinkerman-cat_espurna:gitter.im
about: Also available in Matrix

+ 35
- 0
.github/ISSUE_TEMPLATE/feature-request.yml View File

@ -0,0 +1,35 @@
name: Feature request
description: Suggest an idea for this project
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Before creating a feature request, please search for any existing [issues](https://github.com/xoseperez/espurna/issues) (both open and closed).
Fulfilling this template will help developers and contributors to triage and evalute this request.
- type: textarea
attributes:
label: Description
description: |
Is your feature request related to a problem? Please describe
validations:
required: true
- type: textarea
attributes:
label: Solution
description: |
A clear and concise description of what you want to happen
- type: textarea
attributes:
label: Alternatives
description: |
A clear and concise description of any alternative solutions or features you have considered
- type: textarea
attributes:
label: Additional context
description: |
Add any other information about the feature request (references, screenshots, etc.)

+ 0
- 26
.github/ISSUE_TEMPLATE/feature_request.md View File

@ -1,26 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
*Before creating a new feature request please check that you have searched the existing [issues](https://github.com/xoseperez/espurna/issues) (both open and closed)*
*Fulfilling this template will help developers and contributors evaluating the feature. If the information provided is not enough the issue will likely be closed.*
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the request then you can delete them.*
**Is your feature request related to a problem? Please describe.**
*A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]*
**Describe the solution you'd like**
*A clear and concise description of what you want to happen.*
**Describe alternatives you've considered**
*A clear and concise description of any alternative solutions or features you've considered.*
**Additional context**
*Add any other context or screenshots about the feature request here.*

+ 0
- 42
.github/ISSUE_TEMPLATE/questions---troubleshooting.md View File

@ -1,42 +0,0 @@
---
name: Questions & troubleshooting
about: Anything not a bug or feature request
title: ''
labels: question
assignees: ''
---
*Before creating a new issue please check that you have:*
* *searched the existing [issues](https://github.com/xoseperez/espurna/issues) (both open and closed)*
* *searched the [wiki](https://github.com/xoseperez/espurna/wiki)*
* *asked for help in the [chat](https://gitter.im/tinkerman-cat/espurna)*
* *done the previous things again :)*
*Fulfilling 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.*
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.*
**Question**
*A clear and concise description of what the problem/doubt is.*
**Screenshots**
*If applicable, add screenshots to help explain your problem.*
**Device information**
*Copy-paste here the information as it is outputted by the device. You can get this information by typing `info` via serial, terminal or in the debug tab in the web UI. The relevant information is that surrounded by the scissors-cut lines (`---8<-------`).*
*If you cannot get this info from the device, please answer this questions:*
* *Arduino Core version*
* *ESPurna version*
* *Flash mode*
* *Device brand, model and version*
**Tools used**
* *Desktop operating system*
* *Browser & version*
* *IDE & version*
* *Compiler & version (if not embedded in IDE)*
**Additional context**
*Add any other context about the problem here.*

+ 41
- 0
.github/ISSUE_TEMPLATE/questions.yml View File

@ -0,0 +1,41 @@
name: Questions or troubleshooting
description: When it is neither a bug or a feature request
labels: ["question"]
body:
- type: markdown
attributes:
value: |
* Please search through existing [issues](https://github.com/xoseperez/espurna/issues) (both open and closed)
* Check out our [wiki](https://github.com/xoseperez/espurna/wiki) for any common questions and or issues
* Consider asking at our [Gitter chat](https://gitter.im/tinkerman-cat/espurna) ([which is also accessible with any Matrix client!](https://matrix.to/#/#tinkerman-cat_espurna:gitter.im))
* Re-check our existing issues yet again :)
- type: input
attributes:
label: Device
description: Used development board or device brand, model and version.
placeholder: Sonoff Mini
validations:
required: false
- type: input
id: version
attributes:
label: Version
description: |
ESPurna version, if this question is related to something happening with the firmware
* .bin filename or snapshot name / date, if downloaded from 'Releases'
* 'Firmware version' in WebUI
* First line of `info` terminal command output
* Output of `git rev-parse HEAD`, in the directory that was created when you cloned this repository
placeholder: 1.16.0-dev.git12345678+github250718
validations:
required: false
- type: textarea
attributes:
label: Question
description:
A clear and concise description of what the problem / question / usage issue is.
validations:
required: true

+ 11
- 0
.github/release_template.md View File

@ -0,0 +1,11 @@
# IMPORTANT NOTICE
When updating from 1.14.1 make sure **there is enough available space on the device** (usually, "available space" rounded to the nearest 4096 byte increment) to perform the OTA upgrade. Telnet and direct HTTP upgrades with an .bin too big **will result in a bricked device**. espota.py and Web Interface upgrades will issue a warning.
Make sure to **only** perform OTA upgrade with a properly powered device. Attempting to flash and / or use a normally AC powered device (like a Sonoff) instead powered through 3v3 **may** result in unexpected issues with the firmware.
# Snapshot build
**How to upgrade "over-the-air" aka OTA**: https://github.com/xoseperez/espurna/wiki/OTA
**Latest changes**: https://github.com/xoseperez/espurna/compare/$last_tag...$tag
**CHANGELOG.md**: https://github.com/xoseperez/espurna/blob/$tag/CHANGELOG.md

+ 47
- 32
.github/workflows/push.yml View File

@ -1,27 +1,27 @@
# Check the build status for the nighly builder, release or pull request
name: PlatformIO Build
name: ESPurna build
on: [push, pull_request]
on:
push:
branches:
- dev
- 'test/**'
tags-ignore: '**'
pull_request:
branches: [dev]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('ci_install.sh') }}
- name: Cache PlatformIO
uses: actions/cache@v2
- name: Cache CMake
uses: actions/cache@v3
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('code/platformio.ini') }}
path: code/test/unit/cache
key: ${{ runner.os }}-cmake-${{ hashFiles('code/test/unit/CMakeLists.txt') }}
- name: Host tests
run: |
./ci_install.sh host
@ -30,15 +30,15 @@ jobs:
webui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '18'
- name: Cache Node
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-${{ hashFiles('code/package-lock.json', 'code/package.json') }}
key: ${{ runner.os }}-npm-${{ hashFiles('code/package-lock.json', 'code/package.json') }}
- name: WebUI tests
run: |
./ci_install.sh webui
@ -46,28 +46,43 @@ jobs:
build:
runs-on: ubuntu-latest
env:
ESPURNA_PIO_SHARED_LIBRARIES: "1"
strategy:
matrix:
env: [nodemcu-lolin, esp8266-4m-base, esp8266-4m-latest-base]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Cache pip
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('ci_install.sh') }}
- name: Cache PlatformIO
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('code/platformio.ini') }}
key: ${{ runner.os }}-build-${{ matrix.env }}-${{ hashFiles('code/platformio.ini') }}
restore-keys: |
${{ runner.os }}-build-${{ matrix.env }}-
${{ runner.os }}-build-
- name: Cache libraries
uses: actions/cache@v3
with:
path: libraries/
key: ${{ runner.os }}-libraries-${{ hashFiles('code/platformio.ini') }}
restore-keys: |
${{ runner.os }}-libraries-
- name: PlatformIO prepare
run: |
git config --global advice.detachedHead false
./ci_install.sh build
- run: |
./ci_script.sh build esp8266-4m-base
- run: |
./ci_script.sh build esp8266-4m-latest-base
./ci_install.sh build ${{ matrix.env }}
- if: ${{ matrix.env == 'nodemcu-lolin' }}
name: Basic build
run: |
./ci_script.sh build ${{ matrix.env }}
- if: ${{ endsWith(matrix.env, '-base') }}
name: Test build
run: |
./ci_script.sh test ${{ matrix.env }}

+ 146
- 0
.github/workflows/release.yml View File

@ -0,0 +1,146 @@
# TODO: declare as action so this becomes `uses: blah/espurna-release`?
# ref. https://github.com/mcspr/espurna-nightly-builder/blob/builder/.github/workflows/nightly.yml
# instead of revision + current date, use tag value as full version of the binary
name: Release
on:
workflow_dispatch:
jobs:
variables:
runs-on: ubuntu-latest
outputs:
date: ${{ steps.result.outputs.date }}
dev: ${{ steps.result.outputs.dev }}
master: ${{ steps.result.outputs.master }}
steps:
- name: Prepare version variables
id: result
shell: bash
run: |
revision() {
git ls-remote --exit-code --heads https://github.com/xoseperez/espurna.git refs/heads/$1 | cut -d$'\t' -f1
}
date=$(date +'%y%m%d')
echo "date=${date}" >> "$GITHUB_OUTPUT"
dev=$(revision dev)
echo "dev=${dev}" >> "$GITHUB_OUTPUT"
master=$(revision master)
echo "master=${master}" >> "$GITHUB_OUTPUT"
build:
needs: variables
runs-on: ubuntu-latest
strategy:
matrix:
thread: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v3
with:
path: espurna
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Cache Node
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('espurna/code/package-lock.json', 'espurna/code/package.json') }}
- name: Cache PlatformIO
uses: actions/cache@v3
with:
path: ~/.platformio
key: ${{ runner.os }}-platformio-${{ hashFiles('espurna/code/platformio.ini') }}
- name: Install PlatformIO
run: |
pip install -U platformio
pio upgrade --dev
- name: Build
run: |
git config --global advice.detachedHead false
pushd espurna/code
npm ci
node node_modules/gulp/bin/gulp.js
# each "thread" only builds every Nth environment
# numbers are hard-coded above (...until there's a better idea for doing this)
./scripts/generate_release_sh.py \
--ignore secure-client \
--destination ${GITHUB_WORKSPACE}/build \
--builder-thread ${{ matrix.thread }} \
--builder-total-threads 4 \
--suffix github${{ needs.variables.outputs.date }} \
> release.sh
bash release.sh
popd
- name: Archive
run: |
pushd build
time zip \
--quiet \
--recurse-paths \
../Build_${{ matrix.thread }}.zip ./
popd
- uses: actions/upload-artifact@v3
with:
name: Build
path: Build_${{ matrix.thread }}.zip
upload:
needs: [variables, build]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- uses: actions/download-artifact@v3
with:
path: artifacts/
- name: Unpack
run: unzip -d build "artifacts/Build/*.zip"
- name: Prepare debug info
run: |
time zip \
-9 \
--quiet \
--junk-paths \
--recurse-patterns \
Debug.zip \
'build/debug/*.map' \
'build/debug/*.elf.debug'
- name: Fetch release template
run: |
curl \
-H "Authentication: ${{ secrets.GITUB_TOKEN }}" \
-H "Accept: application/vnd.github.VERSION.raw" \
-o release_template.md \
https://api.github.com/repos/xoseperez/espurna/contents/.github/release_template.md
- uses: ncipollo/release-action@v1
with:
tag: github${{ needs.variables.outputs.date }}
commit: ${{ needs.variables.outputs.dev }}
name: Snapshot build (github${{ needs.variables.outputs.date }})
bodyFile: "release_template.md"
artifacts: "Debug.zip,build/*.bin"
token: ${{ secrets.GITHUB_TOKEN }}
prerelease: true
draft: true

+ 18
- 12
.gitignore View File

@ -1,19 +1,25 @@
*.s#?
*.b#?
.modgit
firmware*
*.gch
.pio*
*.py[cod]
*.s#?
.DS_Store
.clang_complete
.env
.gcc-flags.json
.sconsign.dblite
credentials.h
node_modules
code/utils
custom.h
.modgit
.pio
.python
.env
.DS_Store
.python-version
.sconsign.dblite
.vscode
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
_pycache_/
*.py[cod]
code/.cache/
code/espurna/config/custom.h
code/libraries/
code/node_modules
code/test/unit/cache
compile_commands.json
firmware*

+ 369
- 200
CHANGELOG.md View File

@ -3,226 +3,395 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [1.15.0] In the works
## Pending changes
???
## [1.15.0-dev] Snapshot build 2023-03-30
### Fixed
#### Alexa
- Fix device discovery / state callback ([#2388](https://github.com/xoseperez/espurna/issues/2388)), thanks to **[@aL1aL7](https://github.com/aL1aL7)**
- Display the used name in the WebUI [6b2c34ea](https://github.com/xoseperez/espurna/commit/6b2c34eaae92f196deaaea82ae2864ff2fc6e4cc)
#### Domoticz
- Add workaround for pressure sensors ([#2215](https://github.com/xoseperez/espurna/issues/2215))
- Do not put floats into nvalue ([#2230](https://github.com/xoseperez/espurna/issues/2230))
- Constrain pressure to 0...100 ([#2230](https://github.com/xoseperez/espurna/issues/2230))
- Fix idx truncation when reading from settings ([#2316](https://github.com/xoseperez/espurna/issues/2316), thanks to **[@m-kozlowski](https://github.com/m-kozlowski)**)
- Allow dimmer device to control the brightness ([#2317](https://github.com/xoseperez/espurna/issues/2317), thanks to **[@m-kozlowski](https://github.com/m-kozlowski)**)
- Send co2 ppm as nvalue [d04b85ac](https://github.com/xoseperez/espurna/commit/d04b85ac974f73a68e4e57bce494cda4ac5d6b87)
#### Hardware
- Fix GPIO16 support ([#2110](https://github.com/xoseperez/espurna/issues/2110), thanks to **[@foxman69](https://github.com/foxman69)**)
- Fix for button long click ([#2172](https://github.com/xoseperez/espurna/issues/2172), thanks to **[@ElderJoy](https://github.com/ElderJoy)**)
- Fix latched pulse always being HIGH ([#2145](https://github.com/xoseperez/espurna/issues/2145), thanks to **[@antonio-fiol](https://github.com/antonio-fiol)**)
- Fix ADC\_MODE\_VALUE use in preprocessor ([#2227](https://github.com/xoseperez/espurna/issues/2227), thanks to **[@vtochq](https://github.com/vtochq)**)
- Actually apply button pulldown ([#2239](https://github.com/xoseperez/espurna/issues/2239), thanks to **[@sigmafx](https://github.com/sigmafx)**)
- Fix general.h comment typo ([#2311](https://github.com/xoseperez/espurna/issues/2311), thanks to **[@ruimarinho](https://github.com/ruimarinho)**)
#### I2C
- Make brzo i2c library buildable again [19f32145](https://github.com/xoseperez/espurna/commit/19f3214578ce3429e9140c6a42d1575e4b7fa498)
#### IR
- Fixed build error in case IR TX is not used in raw mode ([#2322](https://github.com/xoseperez/espurna/issues/2322), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
#### Influxdb
- Fix http response parsing, refactor module scope ([#2153](https://github.com/xoseperez/espurna/issues/2153))
#### MQTT
- Set MQTT will topic after /get suffix initialization ([#2106](https://github.com/xoseperez/espurna/issues/2106), [#2115](https://github.com/xoseperez/espurna/issues/2115), thanks to **[@tomas-bara](https://github.com/tomas-bara)**)
#### Nofuss
- Fix nofuss.cpp typo ([#2251](https://github.com/xoseperez/espurna/issues/2251), thanks to **[@CmPi](https://github.com/CmPi)**)
- Bump to 0.4.0 (fork) to support the latest Core version [0422d61c](https://github.com/xoseperez/espurna/commit/0422d61c6969be9963e83850e10b7b217b6e9190)
#### Relay
- Fix sync reentrancy lock [94169dcb](https://github.com/xoseperez/espurna/commit/94169dcbb19b8b83118aaf6c18daf6064cbfa76f)
- Stable configuration IDs [04569c6a](https://github.com/xoseperez/espurna/commit/04569c6a10afe2a662a22c77c7977746f72ea7e1)
#### RPN rules
- rpn $relayX variables were not populated on boot ([#2246](https://github.com/xoseperez/espurna/issues/2246), thanks to **[@pezinek](https://github.com/pezinek)**)
- add missing lights #include for rpn rules ([#2367](https://github.com/xoseperez/espurna/issues/2367), thanks to **[@ngilles](https://github.com/ngilles)**)
#### Sensor
- Apparent, reactive power measurement unit corrections ([#2161](https://github.com/xoseperez/espurna/issues/2161), thanks to **[@irmishappy](https://github.com/irmishappy)**)
- Fixes and updates for thermostat and display ([#2173](https://github.com/xoseperez/espurna/issues/2173), thanks to **[@ElderJoy](https://github.com/ElderJoy)**)
- Properly dispatch emon sensor ratio defaults ([#2241](https://github.com/xoseperez/espurna/issues/2241))
- Set pH decimals to 3 ([#2306](https://github.com/xoseperez/espurna/issues/2306), thanks to **[@ruimarinho](https://github.com/ruimarinho)**)
- Fix setting up GPIO0 as INPUT when preparing to use analogRead(A0) [ff11e581](https://github.com/xoseperez/espurna/commit/ff11e5814ff1fc938afa439c15f57fa9909b9e4a)
- Only change EventSensor counter from the ISR [735e5c0e](https://github.com/xoseperez/espurna/commit/735e5c0ec22fabfdcfa55123b9bceaa3d8f917b8)
- Fix a typo when getting local index for Nth magnitude [6ba5f95e](https://github.com/xoseperez/espurna/commit/6ba5f95e875468f2b7a93c69b45fc6f6c62f390f)
#### TUYA
- Send lights channel value directly [012c3818](https://github.com/xoseperez/espurna/commit/012c3818a59cd46cc89e2affc5ddcdea427a17c1)
- Always run the discovery [2a08ccb2](https://github.com/xoseperez/espurna/commit/2a08ccb2113f8be4495a58aa7b95331591ebcd0b)
#### System
- Rework stability counter [474f0e93](https://github.com/xoseperez/espurna/commit/474f0e93693387f2c85fa28d5df7e0c80716c85a)
- Refactor build configurations [f9211634](https://github.com/xoseperez/espurna/commit/f92116341e141be50f946682404e2d0514fd11f3)
- Clean-up helper classes & functions [ec220b7d](https://github.com/xoseperez/espurna/commit/ec220b7dd1f3b26e81138cec55beec8e37ab35f9)
#### HomeAssistant
- Fix swapped device model and manufacturer fields in the discovery ([#2322](https://github.com/xoseperez/espurna/issues/2322), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
#### Settings
- Fix saving base2 integers [71ddf350](https://github.com/xoseperez/espurna/commit/71ddf35022678667d0269ecc9c60c69bdab68079)
#### Scheduler
- Schedule restore no longer depends on relays [c945c239](https://github.com/xoseperez/espurna/commit/c945c239ea806f8926aefe6262a52177bd089aa5), [e22f67e5](https://github.com/xoseperez/espurna/commit/e22f67e5d61ce8c3c1eb1f9a50f2d6261b8b8d57)
#### WebUI
- Fix scheduler panel tabindex= values ([#2096](https://github.com/xoseperez/espurna/issues/2096), thanks to **[@foxman69](https://github.com/foxman69)**)
- Directly iterate over internal callbacks array ([#2248](https://github.com/xoseperez/espurna/issues/2248), [#2261](https://github.com/xoseperez/espurna/issues/2261))
- Get rid of tabindex= property [14c69a4a](https://github.com/xoseperez/espurna/commit/14c69a4a52cc842c0bf294786ea34904acf0aeec)
- External url clean-up [d60fb47c](https://github.com/xoseperez/espurna/commit/d60fb47ca9be2f591b82f72678b36b02c1c79beb)
- Remove hard-coded group keys list [1627e311](https://github.com/xoseperez/espurna/commit/1627e3119fe9084398b8e8c0ec794b8ed3a4f6b6)
- Send alert messages directly [458fb7d9](https://github.com/xoseperez/espurna/commit/458fb7d936ec9d266cfce275f999dd629fb82e2f)
- Set websocket buffer to `nullptr` before returning control to the webserver, which will try to `free()` it [256e790e](https://github.com/xoseperez/espurna/commit/256e790e4d7374e12430dad57e25ece7b880be25)
- Relay locks can be disabled through API ([d3c113b7](https://github.com/xoseperez/espurna/commit/d3c113b73da27a7ab02df1092345b58d777f9ef4), [78c9e6a2](https://github.com/xoseperez/espurna/commit/78c9e6a218af98351279877d3010ed42c1282765))
- Preserve existing relay locks when using sync modes ([30f3123c](https://github.com/xoseperez/espurna/commit/30f3123cae68588410899f059d13fc19172ebc9c), [d3c113b7](https://github.com/xoseperez/espurna/commit/d3c113b73da27a7ab02df1092345b58d777f9ef4))
- Fix Zero-Or-One and Just-One sync modes causing relays to stay locked forever ([#2574](https://github.com/xoseperez/espurna/issues/2574)) ([11c89789](https://github.com/xoseperez/espurna/commit/11c897898d00c98b274769a60ff3948f4f68d946))
- Disable UART for `MAXCIO_WDE004` ([#2573](https://github.com/xoseperez/espurna/issues/2573))
- MHZ19 out-of-range and timeout error handling ([7bf88654](https://github.com/xoseperez/espurna/commit/7bf886541bd18ec9ed07ec63b26469f829909641))
- MHZ19 calibration and detection range options ([7bf88654](https://github.com/xoseperez/espurna/commit/7bf886541bd18ec9ed07ec63b26469f829909641))
- Do not require DNS server with static IP configuration ([#2582](https://github.com/xoseperez/espurna/issues/2582), [457e47ed](https://github.com/xoseperez/espurna/commit/457e47edd962fc71feb3da8f16c0cd1a9009edc3))
- Fix DNS requests handling with no servers configured or when they are unavailable ([175c04ac](https://github.com/xoseperez/espurna/commit/175c04acc1afeacdd17f7a7721ebaa21ce5ef347))
- Fix thermostat remote sensor payload parser using incorrect keys ([#2585](https://github.com/xoseperez/espurna/issues/2585))
### Added
- WiFi forced sleep mode fixes, allow to properly wakeup from and go into MODEM sleep when disabling both STA and AP modes ([28f3b7da](https://github.com/xoseperez/espurna/commit/28f3b7da84f229c66808ad88635585e132d35d5a), [2f6d7ce3](https://github.com/xoseperez/espurna/commit/2f6d7ce3d37a5cfec3e2fb4b7fa5b4f3fdfe89fa))
- BMx280 suspend sensor measurements when device goes to sleep ([83c9df98](https://github.com/xoseperez/espurna/commit/83c9df9805512a1930351aedfd7da72335ea7a61))
- Accept duration specifiers (h, m, s) in most inputs ([0e861158](https://github.com/xoseperez/espurna/commit/0e8611588b34eea15015c3bcc3473c464920376a), [fd7f97eb](https://github.com/xoseperez/espurna/commit/fd7f97eb2493de3c9756abd09f6abe159f32379d))
- Sensor magnitude decimals / precision setting ([#2550](https://github.com/xoseperez/espurna/issues/2550), [8f1c44fe](https://github.com/xoseperez/espurna/commit/8f1c44fe69383e548cc94a50cccaae71f4ffe056))
- GPIO lock result stored with the request in the log; show WebUI notification when failures occur ([3a1e041f](https://github.com/xoseperez/espurna/commit/3a1e041f5166d4ae459f3ab1853996b2606ee01a))
## [1.15.0-dev] Snapshot build 2023-01-12
### Fixed
- Uninitialized sensor info in Homeassistant discovery causing 'Unknown' entity ([#2572](https://github.com/xoseperez/espurna/issues/2572)) ([d798052d](https://github.com/xoseperez/espurna/commit/d798052dae2a799197a6051f8805bc9bb0884297))
- Fallback AP should be disabled after timeout ([#2571](https://github.com/xoseperez/espurna/issues/2571)) ([f387b864](https://github.com/xoseperez/espurna/commit/f387b864dac87ffb235b893703f93735116dd894))
- Invalid Domoticz settings strings ([#2569](https://github.com/xoseperez/espurna/issues/2569)) by **[@m-kozlowski](https://github.com/m-kozlowski)**
- Lights value adjustments should work with floating point numbers ([#2566](https://github.com/xoseperez/espurna/issues/2566)) by **[@davebuk](https://github.com/davebuk)**
### Added
#### Buttons
- Runtime configuration. (`btnGpio#`, `btnProv#`, etc.)
- Custom action type [ef194c9c](https://github.com/xoseperez/espurna/commit/ef194c9c2430ed14ddf5905214e2968c0f5f9980), [8ceeebdb](https://github.com/xoseperez/espurna/commit/8ceeebdb24aa9bd863fa28676a0364497ec193ae)
#### Garland
- Relay provider for lights state, rework hardware headers to include relay configuration ([#2568](https://github.com/xoseperez/espurna/issues/2568)) ([b5a03dac](https://github.com/xoseperez/espurna/commit/b5a03dac728a4eef34a5c03da5bb395b4d564ed6), [faeedee5](https://github.com/xoseperez/espurna/commit/faeedee57fd84a5466c3a852684a47b76e403f2f))
- IFAN / FAN relay provider and state handling improvements ([73620687](https://github.com/xoseperez/espurna/commit/7362068717baa680adf24ee4e038897134a107e2))
- Even more relay and button providers ([9b28d640](https://github.com/xoseperez/espurna/commit/9b28d640f7d3abe776c24704245f35d941245669), [dca13574](https://github.com/xoseperez/espurna/commit/dca13574d9a00a114b2ec617c732d46c0b8f64c1), [ef086ac0](https://github.com/xoseperez/espurna/commit/ef086ac0fce4c00695a6b9ee8468a2a99fad14cc), [f373e137](https://github.com/xoseperez/espurna/commit/f373e137193ef010db03075e77fb8e19283cd211))
- Publish relay names to MQTT ([7514dee5](https://github.com/xoseperez/espurna/commit/7514dee5b0ff24232098fcac86c49b6cb35c4cf3))
## [1.15.0-dev] Snapshot build 2022-12-12
- Handle telnet socket buffer when eol is missing; fix PuTTY ([542188dd](https://github.com/xoseperez/espurna/commit/542188dd), [53633cfc](https://github.com/xoseperez/espurna/commit/53633cfc))
- Configurable accuracy and sensitivity (mtreg) for BH1750 ([988a9724](https://github.com/xoseperez/espurna/commit/988a9724))
- Power factor allowed values are 0...100 ([52266684](https://github.com/xoseperez/espurna/commit/52266684))
- More GPIO source locations ([b9dcf3e0](https://github.com/xoseperez/espurna/commit/b9dcf3e0))
- API setting relay pulse to zero should stop the timer ([bc2ce115](https://github.com/xoseperez/espurna/commit/bc2ce115))
- Fix `PULSE` terminal command behaviour when we don't want to toggle relay status. Rename `NORMAL STATUS` to `TOGGLE` (`PULSE <ID> <TIME> [<TOGGLE>]`, `TOGGLE` set to `true` by default) ([69471b0a](https://github.com/xoseperez/espurna/commit/69471b0a))
- Fix overflow detection when parsing unsigned numbers ([997c4d01](https://github.com/xoseperez/espurna/commit/997c4d01))
- Add 'value' slider to recover from a fully black RGB color picker ([767cd72e](https://github.com/xoseperez/espurna/commit/767cd72e361302534a70f4586ec69dc59b7cbb2f))
## [1.15.0-dev] Snapshot build 2022-12-04
- Really parse V9261F messages, including writes and write acks ([bea8508e](https://github.com/xoseperez/espurna/commit/bea8508e))
- Increase BH1750 MODE2 decimal places ([b8921b9a](https://github.com/xoseperez/espurna/commit/b8921b9a))
- Separate RGB color temperature in color and non-color modes ([9126a983](https://github.com/xoseperez/espurna/commit/9126a983))
- Revert default PWM frequency to 500Hz (or its equivalent) ([d664b9d4](https://github.com/xoseperez/espurna/commit/d664b9d4))
- Prevent GPIOs configured as relays from being initialized on software reset ([824e1214](https://github.com/xoseperez/espurna/commit/824e1214))
- Fix inverted relays being incorrectly initialized on boot ([d78d7835](https://github.com/xoseperez/espurna/commit/d78d7835))
- Consistent warm and cold lights channel order in hardware header ([144f2a72](https://github.com/xoseperez/espurna/commit/144f2a72))
## [1.15.0-dev] Snapshot build 2022-11-16
- Fix V9261F UART parity mode ([6154f40a](https://github.com/xoseperez/espurna/commit/6154f40a))
- Avoid printing raw network buffers or error strings that are not null-terminated ([7296eca2](https://github.com/xoseperez/espurna/commit/7296eca2))
- Do not update brightness when changing HSV ([2471c16f](https://github.com/xoseperez/espurna/commit/2471c16f))
## [1.15.0-dev] Snapshot build 2022-09-25
- Runtime UART configuration. (`uartBaud#`, `uartInv#`, etc.), see `uart` and `uart <id>` terminal commands output. ([574fbf19](https://github.com/xoseperez/espurna/commit/574fbf196014dbcffa0e99630ea377cce36873e9))
## [1.15.0-dev] Snapshot build 2022-09-23
- Change command-line parser ([ef202109](https://github.com/xoseperez/espurna/commit/ef202109e7e09779d44970e065b71f3cb49660b3) for latest version; ref. [#2245](https://github.com/xoseperez/espurna/issues/2245) and [#2247](https://github.com/xoseperez/espurna/issues/2247) for previous implementation)
- Do not share terminal output. Calling some command(s) in WebUI will no longer stream them to Serial; consistent with the output of API calls. ([ef202109](https://github.com/xoseperez/espurna/commit/ef202109e7e09779d44970e065b71f3cb49660b3))
- Add `pio run -e $env -t .pio/build/$env/firmware.bin.gz` build target ([038f1ffe](https://github.com/xoseperez/espurna/commit/038f1ffec3f61e86fa212450fd2f5ab22823b124))
- Renamed espurna-core to espurna-minimal ([5bc55cd1](https://github.com/xoseperez/espurna/commit/5bc55cd1a5bf60b27aa2c338cd2fd715eb755815))
- Unauthenticated telnet session no longer receives debug messages ([79ef68b4](https://github.com/xoseperez/espurna/commit/79ef68b4355fa71c3261046ba135a52f15bc0df9))
- `__UNIX_TIMESTAMP__` removed; build time remains `__TIMESTAMP__` and as YYYY-MM-DD HH:MM:SS string ([a8fe3e46](https://github.com/xoseperez/espurna/commit/a8fe3e466490f3f6bdda992d74a7f47ffa71e8c9))
## [1.15.0-dev] Snapshot build 2022-08-30
- Remove HWDT exception in RTCMEM, repeated hardware watchdog trigger will eventually trigger SAFE MODE ([59c4246c](https://github.com/xoseperez/espurna/commit/59c4246c6898f4c5698c458ce9762305b43aecde), [7659a634](https://github.com/xoseperez/espurna/commit/7659a63441a7fafff30a274caad65db44de7983a))
- Updated stack decoder script ([a6df38e8](https://github.com/xoseperez/espurna/commit/a6df38e8077c24564c587d2ed8ef871395e47cfe))
- `NOTIFY` terminal command ([cc16c367](https://github.com/xoseperez/espurna/commit/cc16c36736d5de1280c9946a927d8466690292e5))
- Add `pio run -e $env -t build-and-copy`, more configuration options for the version string ([6f122f5e](https://github.com/xoseperez/espurna/commit/6f122f5ecb2af5bb0af0cbdedbe1166f1b92262d), [4c33cacf](https://github.com/xoseperez/espurna/commit/4c33cacfdbe4c51ff52ffb9f530006dfa7037a6b))
## [1.15.0-dev] Snapshot build 2022-08-16
- Move -dev builds from [mcspr/espurna-nightly-builder](https://github.com/mcspr/espurna-nightly-builder) to [xoseperez/espurna](https://github.com/xoseperez/espurna) releases
- Correctly scale NTC value based on input voltage ([#2500](https://github.com/xoseperez/espurna/issues/2500), [1b49326e](https://github.com/xoseperez/espurna/commit/1b49326e12676112163ad5d10aa1574f727b85e7))
- Arduino Core (`analogWrite()`) PWM provider (optional, configured at build-time) ([b0da3e8c](https://github.com/xoseperez/espurna/commit/b0da3e8c7f13e7edc7a092873466e8cd49d83e54), [2365d08b](https://github.com/xoseperez/espurna/commit/2365d08b88bcb810b2c2be3c47eacfee99184fc6))
- Generic conversion for metric units, in both directions ([b366d77a](https://github.com/xoseperez/espurna/commit/b366d77a5e34c520337727bec1b3fa51fe8764b3))
## [1.15.0-dev] Nightly build 2022-05-22
- Magnitude variables may not end in number when there's only one of that type ([5fcac5d2](https://github.com/xoseperez/espurna/commit/5fcac5d27f713ca4a84da3dc8f0b4d10c25c3050))
## [1.15.0-dev] Nightly build 2022-05-18
- Add support for INA219 current and power monitor sensor ([#2510](https://github.com/xoseperez/espurna/issues/2501), [660d8c33](https://github.com/xoseperez/espurna/commit/660d8c339b8bee32575c216cfc78d7b7138c285d), thanks to **[@hamed-ta](https://github.com/hamed-ta)**)
## [1.15.0-dev] Nightly build 2022-05-07
- Add support for Itead Sonoff POW R3 ([#2506](https://github.com/xoseperez/espurna/issues/2506), thanks to **[@nixkj](https://github.com/nixkj)**)
- Add PM1006 air quality sensor, found inside the IKEA VINDRIKTNING ([#2505](https://github.com/xoseperez/espurna/issues/2505), thanks to **[@xoseperez](https://github.com/xoseperez)**)
## [1.15.0-dev] Nightly build 2022-05-05
- Show all of correction / offset settings in the WebUI ([#2491](https://github.com/xoseperez/espurna/issues/2491), [62b2edad](https://github.com/xoseperez/espurna/commit/62b2edadc4649d1e3810c6dd27261e8a45739664))
- Update IRremoteESP8266 to 2.8.2 ([6b1a2de0](https://github.com/xoseperez/espurna/commit/6b1a2de06d0f0b8224f7cb4781bf8ebaafc233c1))
- Updated most of dependencies to use [PlatformIO Registry](https://registry.platformio.org) ([2b50620f](https://github.com/xoseperez/espurna/commit/2b50620f926190712f32a334db6dcc4a5f40e730))
## [1.15.0-dev] Nightly build 2022-01-14
- Don't cancel saving the `relayBootMask` when relays are processed in a certain order ([0c57f0bc](https://github.com/xoseperez/espurna/commit/0c57f0bcf9944e375869544fafe0fc0455964aa4))
## [1.15.0-dev] Nightly build 2021-12-19
- Update websocket client timeout and update message configuration to use seconds instead of milliseconds ([135c7b80](https://github.com/xoseperez/espurna/commit/135c7b80acbfd28136146f08188d81262afd795c))
## [1.15.0-dev] Nightly build 2021-11-26
- Add support for Yagusmart switches ([#2488](https://github.com/xoseperez/espurna/pull/2488), thanks to **[@MelanieT](https://github.com/MelanieT)**)
- Use 64bit microseconds time source for uptime, no need to count overflows manually ([1ca98880](https://github.com/xoseperez/espurna/commit/1ca98880d64db0865d02f009002bc22e32ae5076))
- Update load average and system stability check intervals to use seconds instead of milliseconds ([1ca98880](https://github.com/xoseperez/espurna/commit/1ca98880d64db0865d02f009002bc22e32ae5076))
## [1.15.0-dev] Nightly build 2021-11-23
- SHT3X: add missing I2C address A (0x44) ([#2484](https://github.com/xoseperez/espurna/issues/2484), thanks to **[@drc38](https://github.com/drc38)**)
- Deprecate `..._MIN_CHANGE` and `..._MAX_CHANGE` build flags in favour of per-magnitude runtime settings ([1ef22e16](https://github.com/xoseperez/espurna/commit/1ef22e16f10818d893a0a8912d55b1dbce88fcdb))
- Reduce IRAM usage in sensors using attachInterrupt() ([9db679f9](https://github.com/xoseperez/espurna/commit/9db679f93a61114dec8dad5f2953e59b7663c86a))
- Terminal commands to set expected ratio (`EXPECTED`) and total energy recorded by the sensor (`ENERGY`) [8f7f1c96](https://github.com/xoseperez/espurna/commit/8f7f1c968f92c42f4f80c53ddfb617af18b68a85)
- Remove `PZ.RESET` and `PZ.VALUE` commands in favour of `EXPECTED`, `ENERGY` and `MAGNITUDES` ([8f7f1c96](https://github.com/xoseperez/espurna/commit/8f7f1c968f92c42f4f80c53ddfb617af18b68a85))
## [1.15.0-dev] Nightly build 2021-08-21
- Off-by-one error when formatting to allocated buffer ([efcb863c](https://github.com/xoseperez/espurna/commit/efcb863ca271d3afa2e9accd990bb6adaa3f9652))
- Check the return code of the git process and fail early, when trying to generate the version string ([bdd821db](https://github.com/xoseperez/espurna/commit/bdd821db8609277fef827ce533570818a7614f55))
## [1.15.0-dev] Nightly build 2021-08-16
- Don't treat static PROGMEM and generic C-strings differently ([b167d616](https://github.com/xoseperez/espurna/commit/b167d61615f65b618999d8ed727851a236867b8a), [d9662bd6](https://github.com/xoseperez/espurna/commit/d9662bd66ae9f902707f393a607a07ba713e1199))
- Separate lights IDX from relays, migrate existing configuration from `dczRelayIdx0` to `dczLightIdx` ([94f31241](https://github.com/xoseperez/espurna/commit/94f31241dc42508791d6a582cd163bec33a40a56))
- Update latest Arduino Core platform to 3.0.2 ([1ca8d5e7](https://github.com/xoseperez/espurna/commit/1ca8d5e7a0130c2c23e958208b176bb8e8312d7c))
## [1.15.0-dev] Nightly build 2021-08-06
- HLW8012: gpio runtime configuration ([#2142 (initial Pull Request)](https://github.com/xoseperez/espurna/issues/2142), [fa5e4f7d](https://github.com/xoseperez/espurna/commit/fa5e4f7d06db6bde01f3bebf4f2c8151893c97aa))
- Try to connect to a better AP, when the current RSSI is below -73dBm (only when WiFi scanning is enabled) ([f0f7dcc8](https://github.com/xoseperez/espurna/commit/f0f7dcc874d6f6f4b095b6cb89e69cdb65219150), [dde5f374](https://github.com/xoseperez/espurna/commit/dde5f374dd038afe1fb966d31e16bdac1be581fb), [5a973298 (initial commit)](https://github.com/xoseperez/espurna/commit/5a97329832816219a919c4669e22ad6af0c8d228))
- Allow to set bssid and channel, when scanning is disabled ([c5f70286](https://github.com/xoseperez/espurna/commit/c5f70286d1e63972446c2148914352d9f6acf345))
## [1.15.0-dev] Nightly build 2021-07-20
- Display the device name in the WebUI ([6b2c34ea](https://github.com/xoseperez/espurna/commit/6b2c34eaae92f196deaaea82ae2864ff2fc6e4cc))
- Set websocket buffer to `nullptr` before returning control to the webserver, which will try to `free()` it ([256e790e](https://github.com/xoseperez/espurna/commit/256e790e4d7374e12430dad57e25ece7b880be25))
- Use eslint and html-validate in CI ([433f399d](https://github.com/xoseperez/espurna/commit/433f399d9ce769e57ce660d93161649f6287e054))
- Set MQTT variables just before running the rules ([32b864c5](https://github.com/xoseperez/espurna/commit/32b864c56394016666b716c1623aaf9c85432ed3), [658ce105](https://github.com/xoseperez/espurna/commit/658ce1056e3f11832bce3457c91c0c325c24f509))
- Remove jquery dependencies and clean-up websocket API ([fa3deeff](https://github.com/xoseperez/espurna/commit/fa3deeffbfa622ecd1869af2563940fb3143e94e), [84a7f633](https://github.com/xoseperez/espurna/commit/84a7f6337f72b011512b3e95efe36f2d661e5065), [8e5ab5c9](https://github.com/xoseperez/espurna/commit/8e5ab5c902a23dfd774dd9e768963856d4f26bd3))
## [1.15.0-dev] Nightly build 2021-06-28
- Make brzo i2c library buildable again ([19f32145](https://github.com/xoseperez/espurna/commit/19f3214578ce3429e9140c6a42d1575e4b7fa498), ref. [pasko-zh/brzo\_i2c#44](https://github.com/pasko-zh/brzo_i2c/issues/44))
- Shared ADS1X115 I2CPort, support common gain & data rate settings ([c056c54d](https://github.com/xoseperez/espurna/commit/c056c54db4a528d038584fbfacb8fb410c7c7a2e))
- MDNS auto-connect only works when MQTT is enabled ([06fa5b1c](https://github.com/xoseperez/espurna/commit/06fa5b1c6d3705df48130ad4fe4d946227d4b08e))
## [1.15.0-dev] Nightly build 2021-06-22
- Bump to 0.4.0 (fork) to support the latest Core version ([0422d61c](https://github.com/xoseperez/espurna/commit/0422d61c6969be9963e83850e10b7b217b6e9190))
- Schedule restore no longer depends on relays ([c945c239](https://github.com/xoseperez/espurna/commit/c945c239ea806f8926aefe6262a52177bd089aa5), [e22f67e5](https://github.com/xoseperez/espurna/commit/e22f67e5d61ce8c3c1eb1f9a50f2d6261b8b8d57))
- Fix setting up GPIO0 as INPUT when preparing to use analogRead(A0) ([ff11e581](https://github.com/xoseperez/espurna/commit/ff11e5814ff1fc938afa439c15f57fa9909b9e4a))
- Fix a typo when getting local index for Nth magnitude ([6ba5f95e](https://github.com/xoseperez/espurna/commit/6ba5f95e875468f2b7a93c69b45fc6f6c62f390f))
- Rework build.sh & new release script generator ([75b51f1e](https://github.com/xoseperez/espurna/commit/75b51f1e80260e2325709e7426fc5b2ebd88ada9), [74e18a59](https://github.com/xoseperez/espurna/commit/74e18a59bcbe7a2ea72fccb6d4e5e484bf348bb9))
- Remove Core 2.3.0 support from .ld scripts ([a1e7941f](https://github.com/xoseperez/espurna/commit/a1e7941fa60339fed84f259033523e1e17e3f17d))
- lightfox relay provider & buttonAdd ([bd3a5889](https://github.com/xoseperez/espurna/commit/bd3a588977fb8b195f2bba40618839b617767485))
- Use SoftwareSerial library from the Core ([23da0b74](https://github.com/xoseperez/espurna/commit/23da0b74d403cebc27b6ae0ca520da3218bf7a47))
- Consistent shared libs location with CI and local install, prefer $repo/code/libraries ([f18f128e](https://github.com/xoseperez/espurna/commit/f18f128e4bb718f448ca460cdb0e39545187d7fe), [5ccc70e4](https://github.com/xoseperez/espurna/commit/5ccc70e42e2a3f7410b629d433b301275a149296))
- Further EmonSensor fixes and refactoring ([b19905a3](https://github.com/xoseperez/espurna/commit/b19905a3065672412351c38d859fc3f6cd7ad5cd))
- Rename generic pwr keys with a typed prefix ([1a36efb8](https://github.com/xoseperez/espurna/commit/1a36efb8f2032ac81c5aaa51623a71234b1c4287))
- Tweak analogRead() frequency in Emon sensor ([c136678a](https://github.com/xoseperez/espurna/commit/c136678a4f02b7cae2e59fe843c3910a660f49d1))
## [1.15.0-dev] Nightly build 2021-05-21
- Fix saving base2 integers ([71ddf350](https://github.com/xoseperez/espurna/commit/71ddf35022678667d0269ecc9c60c69bdab68079))
## [1.15.0-dev] Nightly build 2021-05-14
- Add support for SmartMeasure SM300D2-VO2 air quality multi-sensor ([#2447](https://github.com/xoseperez/espurna/issues/2447), thanks to **[@xoseperez](https://github.com/xoseperez)**)
## [1.15.0-dev] Nightly build 2021-05-02
- Fix sync reentrancy lock ([94169dcb](https://github.com/xoseperez/espurna/commit/94169dcbb19b8b83118aaf6c18daf6064cbfa76f))
- Stable configuration IDs ([04569c6a](https://github.com/xoseperez/espurna/commit/04569c6a10afe2a662a22c77c7977746f72ea7e1))
- Rework stability counter ([474f0e93](https://github.com/xoseperez/espurna/commit/474f0e93693387f2c85fa28d5df7e0c80716c85a))
- Refactor build configurations ([f9211634](https://github.com/xoseperez/espurna/commit/f92116341e141be50f946682404e2d0514fd11f3))
- Clean-up helper classes & functions ([ec220b7d](https://github.com/xoseperez/espurna/commit/ec220b7dd1f3b26e81138cec55beec8e37ab35f9))
- Send lights channel value directly ([012c3818](https://github.com/xoseperez/espurna/commit/012c3818a59cd46cc89e2affc5ddcdea427a17c1))
- Always run the discovery ([2a08ccb2](https://github.com/xoseperez/espurna/commit/2a08ccb2113f8be4495a58aa7b95331591ebcd0b))
- Get rid of tabindex= property ([14c69a4a](https://github.com/xoseperez/espurna/commit/14c69a4a52cc842c0bf294786ea34904acf0aeec))
- External url clean-up ([d60fb47c](https://github.com/xoseperez/espurna/commit/d60fb47ca9be2f591b82f72678b36b02c1c79beb))
- Remove hard-coded group keys list ([1627e311](https://github.com/xoseperez/espurna/commit/1627e3119fe9084398b8e8c0ec794b8ed3a4f6b6))
- Send alert messages directly ([458fb7d9](https://github.com/xoseperez/espurna/commit/458fb7d936ec9d266cfce275f999dd629fb82e2f))
- New module for digital LED strips ([#2408](https://github.com/xoseperez/espurna/issues/2408), [c4d817c4](https://github.com/xoseperez/espurna/commit/c4d817c4fba05d70808b234eef3ac5d1ec2bf8c0), [46daa929](https://github.com/xoseperez/espurna/commit/46daa929f5e284877e105208c4e78f7844ae1b64), [d11f82d0](https://github.com/xoseperez/espurna/commit/d11f82d098a69a4a127a8db3218c5643f9831371), [4923377e](https://github.com/xoseperez/espurna/commit/4923377eacc5158896e8fd9ddbc993d1bb2653be), [6508f6bd](https://github.com/xoseperez/espurna/commit/6508f6bda8da2acef555fd2b909b7e2983b97e83), [24550a5b](https://github.com/xoseperez/espurna/commit/24550a5b80e1a626a7d8090746c0cfda2bfb4b23), [4efc417a](https://github.com/xoseperez/espurna/commit/4efc417a39220638079bdf060b0fc204d777f942), [f640cd8e](https://github.com/xoseperez/espurna/commit/f640cd8ecb3d170e9082d60461b89e29454bbbd4), [518d56b4](https://github.com/xoseperez/espurna/commit/518d56b442dda92c267fdb71e6b51b8a27638c0f), [0f73df7c](https://github.com/xoseperez/espurna/commit/0f73df7c36f940040c6a6416c15d39c7eee213be), [3fe68748](https://github.com/xoseperez/espurna/commit/3fe68748637099f08008fa5afc3650e09551285f), [dad8878c](https://github.com/xoseperez/espurna/commit/dad8878ccfcef68f616fed7296b0f07983d855c3), [660ae138](https://github.com/xoseperez/espurna/commit/660ae138d4a40bd3c48058f46d086d396fb217e0), thanks to **[@ElderJoy](https://github.com/ElderJoy)**)
- Add support for Mirabella Genio White A60 globe ([#2439](https://github.com/xoseperez/espurna/issues/2439), [2fc559fa](https://github.com/xoseperez/espurna/commit/2fc559fa5596c6ae3f3cc906177e287c38c6333e), thanks to **[@andrewleech](https://github.com/andrewleech)**)
- Reworked discovery, implement retries and queueing using the MQTT broker ACKs ([59269789](https://github.com/xoseperez/espurna/commit/59269789dc80308e9afc1e4b3051d9d33e13bf8f))
- Scheduler API ([#2431](https://github.com/xoseperez/espurna/issues/2431), thanks to **[@profawk](https://github.com/profawk)**)
- Updated build defaults based on [#2414](https://github.com/xoseperez/espurna/issues/2414) discussion ([92d5e7be](https://github.com/xoseperez/espurna/commit/92d5e7becba23552c836bda8404305a8dc8eb07d))
- Create .map file for the resulting .elf to debug possible compilation issues ([21794b78](https://github.com/xoseperez/espurna/commit/21794b789296683b7ae00a209a42f35ab1023fa1), [1ed00f57](https://github.com/xoseperez/espurna/commit/1ed00f57683197608418a482f0b3b262991856f4))
- Support MQTT wildcards (`#` and `+`) in group subscription topic ([dcc423ec](https://github.com/xoseperez/espurna/commit/dcc423ecaf556082ea7d358b886167f6ad179a21))
- Keep serial disabled in the sonoff rfbridge hardware.h entry ([10519cc2](https://github.com/xoseperez/espurna/commit/10519cc276383b622222a457a19e55d7972d332f))
- Allow to use `<code>,<times>` in rfbON\# / rfbOFF\# settings keys, just like with the API payload ([19947c12](https://github.com/xoseperez/espurna/commit/19947c1231c067301427303c77316565b9163bb4))
- Terminal commands to send the code ([52a244db](https://github.com/xoseperez/espurna/commit/52a244db6e9fe4ad373b580ed4e504c0d84d6afd))
- Add BME680 sensor support ([#2429](https://github.com/xoseperez/espurna/issues/2429), [#2361](https://github.com/xoseperez/espurna/issues/2361), [#2295](https://github.com/xoseperez/espurna/issues/2295), thanks to **[@ruimarinho](https://github.com/ruimarinho)**)
- Controlling global state no longer requires `RELAY_SUPPORT` or specifying a virtual relay in the configuration. Updated modules and APIs to use light controls directly ([2f39d0db](https://github.com/xoseperez/espurna/commit/2f39d0db8a71533dac0cf7c27a719d0097a001d2))
- Do not call the provider or run any transitions when channel values remain unchanged ([2f39d0db](https://github.com/xoseperez/espurna/commit/2f39d0db8a71533dac0cf7c27a719d0097a001d2))
- Set default heartbeat mode to repeat ([f4726d99](https://github.com/xoseperez/espurna/commit/f4726d996636aeaff2e1b62383e2bc5dc00e4a59))
- Simplify NTP tick callback, dont use broker ([13cbc031](https://github.com/xoseperez/espurna/commit/13cbc0310a054309db595451a787bc10f0ab5ca2))
- Remove legacy module based on [NtpClient](https://github.com/gmag11/NtpClient) ([2de44ed5](https://github.com/xoseperez/espurna/commit/2de44ed5d94cc88378b261cebd53c9aa8c4a992e))
- Use direct status update functions instead of broker ([78b4007f](https://github.com/xoseperez/espurna/commit/78b4007f01e8df9334d16e9550a03443527176f2))
- Rework boot info and terminal commands ([7ea73554](https://github.com/xoseperez/espurna/commit/7ea735548bcd41742fac32e8733b2084c4c334cd))
## [1.15.0-dev] Nightly build 2021-01-16
- Custom action type ([ef194c9c](https://github.com/xoseperez/espurna/commit/ef194c9c2430ed14ddf5905214e2968c0f5f9980), [8ceeebdb](https://github.com/xoseperez/espurna/commit/8ceeebdb24aa9bd863fa28676a0364497ec193ae))
- Refactor iFan into a separate module ([a40eca30](https://github.com/xoseperez/espurna/commit/a40eca30ad79315afdb67afa0b0743d4c0087e93))
- Updated build defaults based on [#2414](https://github.com/xoseperez/espurna/issues/2414) discussion ([92d5e7be](https://github.com/xoseperez/espurna/commit/92d5e7becba23552c836bda8404305a8dc8eb07d))
- Use [iro.js](https://github.com/jaames/iro.js) as color picker ([808981ca](https://github.com/xoseperez/espurna/commit/808981ca3938d11d4ddd87005e2881433cc7707b))
- Use [terser](https://github.com/terser/terser) as js minifier, WebUI is no longer limited to ES5 feature set ([cfd6e36d](https://github.com/xoseperez/espurna/commit/cfd6e36dbe94ee0e8098351357f903c060fd5dc9))
## [1.15.0-dev] Nightly build 2021-01-06
- Send co2 ppm as nvalue ([d04b85ac](https://github.com/xoseperez/espurna/commit/d04b85ac974f73a68e4e57bce494cda4ac5d6b87))
- Simplify version + revision into just version ([f0f6f1b8](https://github.com/xoseperez/espurna/commit/f0f6f1b8c907fbf188704e3055210d8202a12f21))
## [1.15.0-dev] Nightly build 2020-12-25
- New module for digital LED strips ([#2408](https://github.com/xoseperez/espurna/issues/2408), [c4d817c4](https://github.com/xoseperez/espurna/commit/c4d817c4fba05d70808b234eef3ac5d1ec2bf8c0), [46daa929](https://github.com/xoseperez/espurna/commit/46daa929f5e284877e105208c4e78f7844ae1b64), [d11f82d0](https://github.com/xoseperez/espurna/commit/d11f82d098a69a4a127a8db3218c5643f9831371), [4923377e](https://github.com/xoseperez/espurna/commit/4923377eacc5158896e8fd9ddbc993d1bb2653be), [6508f6bd](https://github.com/xoseperez/espurna/commit/6508f6bda8da2acef555fd2b909b7e2983b97e83), [24550a5b](https://github.com/xoseperez/espurna/commit/24550a5b80e1a626a7d8090746c0cfda2bfb4b23), [4efc417a](https://github.com/xoseperez/espurna/commit/4efc417a39220638079bdf060b0fc204d777f942), [f640cd8e](https://github.com/xoseperez/espurna/commit/f640cd8ecb3d170e9082d60461b89e29454bbbd4), [518d56b4](https://github.com/xoseperez/espurna/commit/518d56b442dda92c267fdb71e6b51b8a27638c0f), [0f73df7c](https://github.com/xoseperez/espurna/commit/0f73df7c36f940040c6a6416c15d39c7eee213be), [3fe68748](https://github.com/xoseperez/espurna/commit/3fe68748637099f08008fa5afc3650e09551285f), [dad8878c](https://github.com/xoseperez/espurna/commit/dad8878ccfcef68f616fed7296b0f07983d855c3), [660ae138](https://github.com/xoseperez/espurna/commit/660ae138d4a40bd3c48058f46d086d396fb217e0), thanks to **[@ElderJoy](https://github.com/ElderJoy)**)
#### Debug
- Optionally store boot log ([#2109](https://github.com/xoseperez/espurna/issues/2109))
- Log mode, allow to skip boot messages ([#2116](https://github.com/xoseperez/espurna/issues/2116))
#### HTTP API
- Handle received data as terminal command [#2247](https://github.com/xoseperez/espurna/issues/2247))
## [1.15.0-dev] Nightly build 2020-12-06
- Rework plain and JSON implementations ([#2405](https://github.com/xoseperez/espurna/issues/2405))
## [1.15.0-dev] Nightly build 2020-11-29
- Default Emon ratios at compile time ([12ae9d15](https://github.com/xoseperez/espurna/commit/12ae9d15be3f282c30bd5f6b39680d4de1e0ca85))
## [1.15.0-dev] Nightly build 2020-11-03
- Add support for Gosund P1 Power Strip ([#2391](https://github.com/xoseperez/espurna/issues/2391), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
## [1.15.0-dev] Nightly build 2020-10-29
- Fix device discovery / state callback ([#2388](https://github.com/xoseperez/espurna/issues/2388), thanks to **[@aL1aL7](https://github.com/aL1aL7)**)
## [1.15.0-dev] Nightly build 2020-10-16
- Add support for LSC E27 10W white bulb ([#2375](https://github.com/xoseperez/espurna/issues/2375), thanks to **[@tom-kaltofen](https://github.com/tom-kaltofen)**)
- Add support for Benexmart GU5.3 RGBWW light ([#2381](https://github.com/xoseperez/espurna/issues/2381), thanks to **[@ngilles](https://github.com/ngilles)**)
## [1.15.0-dev] Nightly build 2020-10-01
- Only change EventSensor counter from the ISR ([735e5c0e](https://github.com/xoseperez/espurna/commit/735e5c0ec22fabfdcfa55123b9bceaa3d8f917b8))
## [1.15.0-dev] Nightly build 2020-09-27
- add missing lights #include for rpn rules ([#2367](https://github.com/xoseperez/espurna/issues/2367), thanks to **[@ngilles](https://github.com/ngilles)**)
- Detect Tasmota magic numbers when booting, and do a preventive factory reset ([#2370](https://github.com/xoseperez/espurna/issues/2370))
## [1.15.0-dev] Nightly build 2020-09-25
- Resistor ladder / analog buttons support ([#2357](https://github.com/xoseperez/espurna/issues/2357))
- Add support for Gosund SP111 (hardware version 1.1 16A) ([#2360](https://github.com/xoseperez/espurna/issues/2360), [#2369](https://github.com/xoseperez/espurna/issues/2369), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
- system operators `sleep` and `rtcmem` ([#2366](https://github.com/xoseperez/espurna/issues/2366))
## [1.15.0-dev] Nightly build 2020-09-16
- Add support for Fcmila E27 7W RGB+W light bulb ([#2353](https://github.com/xoseperez/espurna/issues/2353), thanks to **[@user176176](https://github.com/user176176)**)
## [1.15.0-dev] Nightly build 2020-09-05
- Add support for the Zhilde ZLD-64EU-W ([#2342](https://github.com/xoseperez/espurna/issues/2342), thanks to **[@biot](https://github.com/biot)**)
## [1.15.0-dev] Nightly build 2020-09-01
- [Prometheus](https://prometheus.io/) metrics support ([#2332](https://github.com/xoseperez/espurna/issues/2332))
- Scheduler API ([#2431](https://github.com/xoseperez/espurna/issues/2431)), thanks to **[@profawk](https://github.com/profawk)**
#### Hardware
- KingArt WiFi Curtain Switch ([#2063](https://github.com/xoseperez/espurna/issues/2063), thanks to **[@AlbertWeterings](https://github.com/AlbertWeterings)**)
- Add support for Kogan Smarter Home Plug With Energy Meter ([#2086](https://github.com/xoseperez/espurna/issues/2086), thanks to **[@aureq](https://github.com/aureq)**)
- Add support for Teckin SB53 smart bulb ([#2090](https://github.com/xoseperez/espurna/issues/2090), thanks to **[@marcuswinkler](https://github.com/marcuswinkler)**)
- Add Shelly 1PM GPIO picture ([#2092](https://github.com/xoseperez/espurna/issues/2092), thanks to **[@lblabr](https://github.com/lblabr)**)
- Add MagicHome ZJ\_LB\_RGBWW\_L support ([#2100](https://github.com/xoseperez/espurna/issues/2100), thanks to **[@wwilsman](https://github.com/wwilsman)**)
- Deltaco smart home devices ([#2103](https://github.com/xoseperez/espurna/issues/2103), thanks to **[@orrpan](https://github.com/orrpan)**)
- Added hardware config for Avatto NAS-WR01W ([#2113](https://github.com/xoseperez/espurna/issues/2113), thanks to **[@blockmar](https://github.com/blockmar)**)
- Config for Teckin SP23 & Maxcio W-UK007S ([#2157](https://github.com/xoseperez/espurna/issues/2157), thanks to **[@julianwb](https://github.com/julianwb)**)
- Add support for read PIO-A of DS2406 ([#2174](https://github.com/xoseperez/espurna/issues/2174), thanks to **[@rmcbc](https://github.com/rmcbc)**)
- Example for Generic ESP01 boards with 512KiB flash ([#2185](https://github.com/xoseperez/espurna/issues/2185), thanks to **[@ziggurat29](https://github.com/ziggurat29)**)
- Board definition for the Gosund WP3 smart socket ([#2191](https://github.com/xoseperez/espurna/issues/2191), thanks to **[@ziggurat29](https://github.com/ziggurat29)**)
- correct Gosund WP3 LED documentation and provide reasonable default actions ([#2200](https://github.com/xoseperez/espurna/issues/2200), thanks to **[@ziggurat29](https://github.com/ziggurat29)**)
- Add support for HUGOAI smart socket plug. ([#2243](https://github.com/xoseperez/espurna/issues/2243), thanks to **[@estebanz01](https://github.com/estebanz01)**)
- Add support for Aoycocr X5P Plug. ([#2235](https://github.com/xoseperez/espurna/issues/2235), thanks to **[@estebanz01](https://github.com/estebanz01)**)
## [1.15.0-dev] Nightly build 2020-08-27
- rfbridge operators (`rfb_send`, `rfb_pop`, `rfb_info`, `rfb_sequence`, `rfb_match`, `rfb_match_wait`) and mqtt fixes ([#2302](https://github.com/xoseperez/espurna/issues/2302))
- Use [fork of rc-switch](https://github.com/1technophile/rc-switch) ([7a24806a](https://github.com/xoseperez/espurna/commit/7a24806adb2c3e2357171e004b5b760daf3bdca4))
## [1.15.0-dev] Nightly build 2020-08-15
- Fix swapped device model and manufacturer fields in the discovery ([#2322](https://github.com/xoseperez/espurna/issues/2322), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
- Fixed build error in case IR TX is not used in raw mode ([#2322](https://github.com/xoseperez/espurna/issues/2322), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
## [1.15.0-dev] Nightly build 2020-08-14
- Allow dimmer device to control the brightness ([#2317](https://github.com/xoseperez/espurna/issues/2317), thanks to **[@m-kozlowski](https://github.com/m-kozlowski)**)
## [1.15.0-dev] Nightly build 2020-08-12
- Fix idx truncation when reading from settings ([#2316](https://github.com/xoseperez/espurna/issues/2316), thanks to **[@m-kozlowski](https://github.com/m-kozlowski)**)
## [1.15.0-dev] Nightly build 2020-08-03
- Fix general.h comment typo ([#2311](https://github.com/xoseperez/espurna/issues/2311), thanks to **[@ruimarinho](https://github.com/ruimarinho)**)
## [1.15.0-dev] Nightly build 2020-07-20
- Set pH decimals to 3 ([#2306](https://github.com/xoseperez/espurna/issues/2306), thanks to **[@ruimarinho](https://github.com/ruimarinho)**)
## [1.15.0-dev] Nightly build 2020-07-17
- Implement support for ProDino WIFI ([#2269](https://github.com/xoseperez/espurna/issues/2269), thanks to **[@dpeddi](https://github.com/dpeddi)**)
## [1.15.0-dev] Nightly build 2020-06-28
- Add support for AG-L4 v3 ([#2276](https://github.com/xoseperez/espurna/issues/2276)), thanks to **[@andrej-peterka](https://github.com/andrej-peterka)**
- Including support for Arlec PC190HA/PB89HA ([#2286](https://github.com/xoseperez/espurna/issues/2286), thanks to **[@mafrosis](https://github.com/mafrosis)**)
- Add support for the Zhilde ZLD-64EU-W ([#2342](https://github.com/xoseperez/espurna/issues/2342), thanks to **[@biot](https://github.com/biot)**)
- Add support for Fcmila E27 7W RGB+W light bulb ([#2353](https://github.com/xoseperez/espurna/issues/2353), thanks to **[@user176176](https://github.com/user176176)**)
- Resistor ladder / analog buttons support ([#2357](https://github.com/xoseperez/espurna/issues/2357))
- Add support for Gosund SP111 (hardware version 1.1 16A) ([#2360](https://github.com/xoseperez/espurna/issues/2360), [#2369](https://github.com/xoseperez/espurna/issues/2369), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
- Add support for LSC E27 10W white bulb ([#2375](https://github.com/xoseperez/espurna/issues/2375), thanks to **[@tom-kaltofen](https://github.com/tom-kaltofen)**)
- Add support for Benexmart GU5.3 RGBWW light ([#2381](https://github.com/xoseperez/espurna/issues/2381), thanks to **[@ngilles](https://github.com/ngilles)**)
- Add support for Gosund P1 Power Strip ([#2391](https://github.com/xoseperez/espurna/issues/2391), thanks to **[@alextircovnicu](https://github.com/alextircovnicu)**)
- Add support for Mirabella Genio White A60 globe ([#2439](https://github.com/xoseperez/espurna/issues/2439]) [2fc559fa](https://github.com/xoseperez/espurna/commit/2fc559fa5596c6ae3f3cc906177e287c38c6333e), thanks to **[@andrewleech](https://github.com/andrewleech)**)
- Refactor iFan into a separate module [a40eca30](https://github.com/xoseperez/espurna/commit/a40eca30ad79315afdb67afa0b0743d4c0087e93)
#### HomeAssistant
- Reworked discovery [59269789](https://github.com/xoseperez/espurna/commit/59269789dc80308e9afc1e4b3051d9d33e13bf8f)
- Advertise lights transition support in discovery message
#### TUYA
- Updated build defaults based on [#2414](https://github.com/xoseperez/espurna/issues/2414) discussion [92d5e7be](https://github.com/xoseperez/espurna/commit/92d5e7becba23552c836bda8404305a8dc8eb07d)
#### MQTT
## [1.15.0-dev] Nightly build 2020-05-27
- Refactor WS implementation, add some comments to the header ([#2261](https://github.com/xoseperez/espurna/issues/2261))
## [1.15.0-dev] Nightly build 2020-05-26
- Handle received data as terminal command ([#2247](https://github.com/xoseperez/espurna/issues/2247))
#### PlatformIO
- Use development version of PlatformIO Core in CI ([#2146](https://github.com/xoseperez/espurna/issues/2146), thanks to **[@ivankravets](https://github.com/ivankravets)**)
- Handle received payload as terminal input (by default, `<root topic>/cmd`) ([#2247](https://github.com/xoseperez/espurna/issues/2247))
## [1.15.0-dev] Nightly build 2020-05-22
- Add '.example' files. ([#2257](https://github.com/xoseperez/espurna/issues/2257), thanks to **[@davebuk](https://github.com/davebuk)**)
- Create .map file for the resulting .elf to debug possible compilation issues [21794b78](https://github.com/xoseperez/espurna/commit/21794b789296683b7ae00a209a42f35ab1023fa1), [1ed00f57](https://github.com/xoseperez/espurna/commit/1ed00f57683197608418a482f0b3b262991856f4)
#### Relay
- Runtime configuration. (`relayGpio#`, `relayProv#`, etc.)
#### RPN Rules
- `oneshot_ms`, `every_ms` timer support.
- rfbridge operators (`rfb_send`, `rfb_pop`, `rfb_info`, `rfb_sequence`, `rfb_match`, `rfb_match_wait`) and mqtt fixes ([#2302](https://github.com/xoseperez/espurna/issues/2302))
- system operators `sleep` and `rtcmem` ([#2366](https://github.com/xoseperez/espurna/issues/2366))
#### Sensors
- HLW8012: gpio runtime configuration ([#2142](https://github.com/xoseperez/espurna/issues/2142))
- Add SI1145 sensor ([#2216](https://github.com/xoseperez/espurna/issues/2216), thanks to **[@HilverinkJ](https://github.com/HilverinkJ)**)
- Add HDC1080 sensor ([#2227](https://github.com/xoseperez/espurna/issues/2227), thanks to **[@vtochq](https://github.com/vtochq)**)
- HLW8012: energy\_delta ([#2230](https://github.com/xoseperez/espurna/issues/2230))
- Load ratios after boot + show pwr defaults with `get` ([#2241](https://github.com/xoseperez/espurna/issues/2241))
- Default Emon ratios at compile time [12ae9d15](https://github.com/xoseperez/espurna/commit/12ae9d15be3f282c30bd5f6b39680d4de1e0ca85)
- Add BME680 sensor support ([#2429](https://github.com/xoseperez/espurna/issues/2429), [#2361](https://github.com/xoseperez/espurna/issues/2361), [#2295](https://github.com/xoseperez/espurna/issues/2295), thanks to **[@ruimarinho](https://github.com/ruimarinho)**)
- Add support for SmartMeasure SM300D2-VO2 air quality multi-sensor ([#2447](https://github.com/xoseperez/espurna/issues/2447), thanks to **[@xoseperez](https://github.com/xoseperez)**)
- Shared ADS1X115 I2CPort, support common gain & data rate settings [c056c54d](https://github.com/xoseperez/espurna/commit/c056c54db4a528d038584fbfacb8fb410c7c7a2e)
#### Settings
- Led and button GPIO runtime settings ([#2117](https://github.com/xoseperez/espurna/issues/2117), [#2162](https://github.com/xoseperez/espurna/issues/2162), [#2170](https://github.com/xoseperez/espurna/issues/2170), [#2177](https://github.com/xoseperez/espurna/issues/2177))
- Configure light dimmer pins from settings ([#2129](https://github.com/xoseperez/espurna/issues/2129))
#### System
- Detect Tasmota magic numbers when booting, and do a preventive factory reset ([#2370](https://github.com/xoseperez/espurna/issues/2370))
#### Terminal
- Show pretty uptime with NTP\_SUPPORT ([#2137](https://github.com/xoseperez/espurna/issues/2137))
- Change command-line parser ([#2245](https://github.com/xoseperez/espurna/issues/2245), [#2247](https://github.com/xoseperez/espurna/issues/2247))
#### Thingspeak
- Configure Thingspeak URL at runtime ([#2124](https://github.com/xoseperez/espurna/issues/2124), thanks to **[@sametflo](https://github.com/sametflo)**)
- Refactor deprecated WiFiClientSecure ([#2140](https://github.com/xoseperez/espurna/issues/2140), [#2144](https://github.com/xoseperez/espurna/issues/2144))
#### WebUI
- WebUI: alert when WS closes ([#2131](https://github.com/xoseperez/espurna/issues/2131), thanks to **[@foxman69](https://github.com/foxman69)**)
- Optional Web(UI) OTA ([#2190](https://github.com/xoseperez/espurna/issues/2190))
## [1.15.0-dev] Nightly build 2020-05-18
- Fix nofuss.cpp typo ([#2251](https://github.com/xoseperez/espurna/issues/2251), thanks to **[@CmPi](https://github.com/CmPi)**)
- Kingart curtain switch UI support ([#2250](https://github.com/xoseperez/espurna/issues/2250), thanks to **[@echauvet](https://github.com/echauvet)**)
- Refactor WS implementation, add some comments to the header ([#2261](https://github.com/xoseperez/espurna/issues/2261))
#### WiFi
- SoftAP DHCP leases ([#2320](https://github.com/xoseperez/espurna/issues/2320))
- Try to connect to a better AP, when the current RSSI is below -73dBm (only when WiFi scanning is enabled).
#### Relays
- Support multiple provider types (GPIO, virtual, IO expanders, etc.)
- Separate MQTT group subscription and publish topics.
- Support MQTT wildcards (`#` and `+`) in group subscription topic [dcc423ec](https://github.com/xoseperez/espurna/commit/dcc423ecaf556082ea7d358b886167f6ad179a21)
#### RFBridge
- Keep serial disabled in the sonoff rfbridge hardware.h entry [10519cc2](https://github.com/xoseperez/espurna/commit/10519cc276383b622222a457a19e55d7972d332f)
- Allow to use `<code>,<times>` in rfbON\# / rfbOFF\# settings keys, just like with the API payload [19947c12](https://github.com/xoseperez/espurna/commit/19947c1231c067301427303c77316565b9163bb4)
- Terminal commands to send the code [52a244db](https://github.com/xoseperez/espurna/commit/52a244db6e9fe4ad373b580ed4e504c0d84d6afd)
- Support all available relay providers, not just the DUMMY variant. Allow to control real GPIO relays with received RF codes.
### Changed
#### Build
- Convert .ino -> .cpp ([#1306](https://github.com/xoseperez/espurna/issues/1306), [#2228](https://github.com/xoseperez/espurna/issues/2228), [#2234](https://github.com/xoseperez/espurna/issues/2234), [#2236](https://github.com/xoseperez/espurna/issues/2236))
- Rework build.sh & new release script generator [75b51f1e](https://github.com/xoseperez/espurna/commit/75b51f1e80260e2325709e7426fc5b2ebd88ada9), [74e18a59](https://github.com/xoseperez/espurna/commit/74e18a59bcbe7a2ea72fccb6d4e5e484bf348bb9)
- Use python 3.x in CI and move to Github Actions.
- Use eslint and html-validate in CI [433f399d](https://github.com/xoseperez/espurna/commit/433f399d9ce769e57ce660d93161649f6287e054)
- Simplify version + revision into just version [f0f6f1b8](https://github.com/xoseperez/espurna/commit/f0f6f1b8c907fbf188704e3055210d8202a12f21)
- Remove Core 2.3.0 support from .ld scripts [a1e7941f](https://github.com/xoseperez/espurna/commit/a1e7941fa60339fed84f259033523e1e17e3f17d)
#### PlatformIO
- Update latest Arduino Core platform to 3.0.1 ([68436f1e](https://github.com/xoseperez/espurna/commit/68436f1e72f87fa90ae36ecfd82bf8cc516f7f02))
- Use SoftwareSerial library from the Core ([23da0b74](https://github.com/xoseperez/espurna/commit/23da0b74d403cebc27b6ae0ca520da3218bf7a47))
- Remove -ota envs, handle OTA condition in extra script ([#2099](https://github.com/xoseperez/espurna/issues/2099))
## [1.15.0-dev] Nightly build 2020-05-13
- rpn $relayX variables were not populated on boot ([#2246](https://github.com/xoseperez/espurna/issues/2246), thanks to **[@pezinek](https://github.com/pezinek)**)
- Properly dispatch emon sensor ratio defaults ([#2241](https://github.com/xoseperez/espurna/issues/2241))
- Directly iterate over internal callbacks array ([#2248](https://github.com/xoseperez/espurna/issues/2248), [#2261](https://github.com/xoseperez/espurna/issues/2261))
- Add support for HUGOAI smart socket plug. ([#2243](https://github.com/xoseperez/espurna/issues/2243), thanks to **[@estebanz01](https://github.com/estebanz01)**)
- Load ratios after boot + show pwr defaults with `get` ([#2241](https://github.com/xoseperez/espurna/issues/2241))
## [1.15.0-dev] Nightly build 2020-05-02
- KingArt WiFi Curtain Switch ([#2063](https://github.com/xoseperez/espurna/issues/2063), thanks to **[@AlbertWeterings](https://github.com/AlbertWeterings)**)
## [1.15.0-dev] Nightly build 2020-05-01
- Add support for Aoycocr X5P Plug. ([#2235](https://github.com/xoseperez/espurna/issues/2235), thanks to **[@estebanz01](https://github.com/estebanz01)**)
## [1.15.0-dev] Nightly build 2020-04-29
- Do not put floats into nvalue ([#2230](https://github.com/xoseperez/espurna/issues/2230))
- Constrain pressure to 0...100 ([#2230](https://github.com/xoseperez/espurna/issues/2230))
- HLW8012: energy\_delta ([#2230](https://github.com/xoseperez/espurna/issues/2230))
## [1.15.0-dev] Nightly build 2020-04-26
- Fix ADC\_MODE\_VALUE use in preprocessor ([#2227](https://github.com/xoseperez/espurna/issues/2227), thanks to **[@vtochq](https://github.com/vtochq)**)
- Add HDC1080 sensor ([#2227](https://github.com/xoseperez/espurna/issues/2227), thanks to **[@vtochq](https://github.com/vtochq)**)
## [1.15.0-dev] Nightly build 2020-04-08
### Sensors
- Emon refactoring ([#2213](https://github.com/xoseperez/espurna/issues/2213))
- Add workaround for pressure sensors ([#2215](https://github.com/xoseperez/espurna/issues/2215))
- Add SI1145 sensor ([#2216](https://github.com/xoseperez/espurna/issues/2216), thanks to **[@HilverinkJ](https://github.com/HilverinkJ)**)
### PlatformIO
- platformio.ini refactoring ([#2212](https://github.com/xoseperez/espurna/issues/2212))
- Rename generic environments ([#2214](https://github.com/xoseperez/espurna/issues/2214))
- esp8266-\<flavour\>-\<size\>-base to esp8266-\<size\>-\<flavour\>-base
- espurna-base to espurna-core-webui
- Consistent shared libs location with CI and local install, prefer $repo/code/libraries [f18f128e](https://github.com/xoseperez/espurna/commit/f18f128e4bb718f448ca460cdb0e39545187d7fe)
- Pin libraries versions as \<owner\>/\<name\> to fix possible issues with Trusted Package Registry [a9220ec2b](https://github.com/xoseperez/espurna/commit/a9220ec2b27224b2da79880945f6f58450ba53e8)
- Add `pio run -e $env -t build-and-copy`, more configuration options for the version string [4c33cacf](https://github.com/xoseperez/espurna/commit/4c33cacfdbe4c51ff52ffb9f530006dfa7037a6b)
#### HTTP API
- Rework plain and JSON implementations ([#2405](https://github.com/xoseperez/espurna/issues/2405))
#### Libraries
## [1.15.0-dev] Nightly build 2020-04-02
- Gosund WP3 LED documentation and provide reasonable default actions ([#2200](https://github.com/xoseperez/espurna/issues/2200), thanks to **[@ziggurat29](https://github.com/ziggurat29)**)
## [1.15.0-dev] Nightly build 2020-03-31
- Board definition for the Gosund WP3 smart socket ([#2191](https://github.com/xoseperez/espurna/issues/2191), thanks to **[@ziggurat29](https://github.com/ziggurat29)**)
## [1.15.0-dev] Nightly build 2020-03-21
- Example for Generic ESP01 boards with 512KiB flash ([#2185](https://github.com/xoseperez/espurna/issues/2185), thanks to **[@ziggurat29](https://github.com/ziggurat29)**)
- Allow to disable Web(UI) OTA support ([#2190](https://github.com/xoseperez/espurna/issues/2190))
- Always buffer incoming MQTT ([#2181](https://github.com/xoseperez/espurna/issues/2181))
## [1.15.0-dev] Nightly build 2020-03-13
- Apparent, reactive power measurement unit corrections ([#2161](https://github.com/xoseperez/espurna/issues/2161), thanks to **[@irmishappy](https://github.com/irmishappy)**)
- Add support for read PIO-A of DS2406 ([#2174](https://github.com/xoseperez/espurna/issues/2174), thanks to **[@rmcbc](https://github.com/rmcbc)**)
- Update migrate configuration & conditions, allow each module to access the current & previous version ([#2176](https://github.com/xoseperez/espurna/issues/2176))
## [1.15.0-dev] Nightly build 2020-03-08
- Fix for button long click ([#2172](https://github.com/xoseperez/espurna/issues/2172), thanks to **[@ElderJoy](https://github.com/ElderJoy)**)
- Fixes and updates for thermostat and display ([#2173](https://github.com/xoseperez/espurna/issues/2173), thanks to **[@ElderJoy](https://github.com/ElderJoy)**)
## [1.15.0-dev] Nightly build 2020-02-20
### PlatformIO
- Use development version of PlatformIO Core in CI ([#2146](https://github.com/xoseperez/espurna/issues/2146), thanks to **[@ivankravets](https://github.com/ivankravets)**)
- Bump RFM69 version ([#2148](https://github.com/xoseperez/espurna/issues/2148))
- Pin arduino-mqtt version ([#2154](https://github.com/xoseperez/espurna/issues/2154))
- Update IRremoteESP8266 to 2.7.4 ([#2182](https://github.com/xoseperez/espurna/issues/2182))
- Use [fork of fauxmoesp](https://github.com/vintlabs/fauxmoESP), thanks to **[@m-kozlowski](https://github.com/m-kozlowski)**
- Use [fork of rc-switch](https://github.com/1technophile/rc-switch) [7a24806a](https://github.com/xoseperez/espurna/commit/7a24806adb2c3e2357171e004b5b760daf3bdca4)
#### MQTT
### System
- Fix http response parsing, refactor module scope ([#2153](https://github.com/xoseperez/espurna/issues/2153))
- Set keepalive to be less than heartbeat interval ([#2154](https://github.com/xoseperez/espurna/issues/2154))
- Always buffer incoming data ([#2181](https://github.com/xoseperez/espurna/issues/2181))
- Set default heartbeat mode to repeat [f4726d99](https://github.com/xoseperez/espurna/commit/f4726d996636aeaff2e1b62383e2bc5dc00e4a59)
- MDNS auto-connect only works when MQTT is enabled [06fa5b1c](https://github.com/xoseperez/espurna/commit/06fa5b1c6d3705df48130ad4fe4d946227d4b08e)
#### NTP
### Hardware
- Config for Teckin SP23 & Maxcio W-UK007S ([#2157](https://github.com/xoseperez/espurna/issues/2157), thanks to **[@julianwb](https://github.com/julianwb)**)
- Fix latched pulse always being HIGH ([#2145](https://github.com/xoseperez/espurna/issues/2145), thanks to **[@antonio-fiol](https://github.com/antonio-fiol)**)
## [1.15.0-dev] Nightly build 2020-02-09
- Configure Thingspeak URL at runtime ([#2124](https://github.com/xoseperez/espurna/issues/2124), thanks to **[@sametflo](https://github.com/sametflo)**)
- Refactor deprecated WiFiClientSecure ([#2140](https://github.com/xoseperez/espurna/issues/2140), [#2144](https://github.com/xoseperez/espurna/issues/2144))
- WebUI: alert when web socket connection closes ([#2131](https://github.com/xoseperez/espurna/issues/2131), thanks to **[@foxman69](https://github.com/foxman69)**)
## [1.15.0-dev] Nightly build 2020-02-05
- Show pretty uptime string with `NTP_SUPPORT=1` ([#2137](https://github.com/xoseperez/espurna/issues/2137))
## [1.15.0-dev] Nightly build 2020-02-04
- Configure light dimmer pins from settings ([#2129](https://github.com/xoseperez/espurna/issues/2129))
- Use sntp app from lwip on latest Cores, replace NtpClient ([#2132](https://github.com/xoseperez/espurna/issues/2132))
- Simplify NTP tick callback, dont use broker [13cbc031](https://github.com/xoseperez/espurna/commit/13cbc0310a054309db595451a787bc10f0ab5ca2)
- Remove legacy module based on [NtpClient](https://github.com/gmag11/NtpClient) [2de44ed5](https://github.com/xoseperez/espurna/commit/2de44ed5d94cc88378b261cebd53c9aa8c4a992e)
- Updates to support 64bit time\_t.
#### System
- Use direct status updates instead of broker [78b4007f](https://github.com/xoseperez/espurna/commit/78b4007f01e8df9334d16e9550a03443527176f2)
#### Sensors
- Emon refactoring ([#2213](https://github.com/xoseperez/espurna/issues/2213))
- Further EmonSensor fixes and refactoring [b19905a3](https://github.com/xoseperez/espurna/commit/b19905a3065672412351c38d859fc3f6cd7ad5cd)
- Rename generic pwr keys with a typed prefix [1a36efb8](https://github.com/xoseperez/espurna/commit/1a36efb8f2032ac81c5aaa51623a71234b1c4287)
- Tweak analogRead() frequency in Emon sensor [c136678a](https://github.com/xoseperez/espurna/commit/c136678a4f02b7cae2e59fe843c3910a660f49d1)
#### Terminal
- Rework boot info and terminal commands [7ea73554](https://github.com/xoseperez/espurna/commit/7ea735548bcd41742fac32e8733b2084c4c334cd)
#### RPN Rules
- Set MQTT variables just before running the rules [32b864c5](https://github.com/xoseperez/espurna/commit/32b864c56394016666b716c1623aaf9c85432ed3), [658ce105](https://github.com/xoseperez/espurna/commit/658ce1056e3f11832bce3457c91c0c325c24f509)
#### WebUI
- Remove jquery dependencies and clean-up websocket API [fa3deeff](https://github.com/xoseperez/espurna/commit/fa3deeffbfa622ecd1869af2563940fb3143e94e), [84a7f633](https://github.com/xoseperez/espurna/commit/84a7f6337f72b011512b3e95efe36f2d661e5065), [8e5ab5c9](https://github.com/xoseperez/espurna/commit/8e5ab5c902a23dfd774dd9e768963856d4f26bd3)
- Use [iro.js](https://github.com/jaames/iro.js) as color picker [808981ca](https://github.com/xoseperez/espurna/commit/808981ca3938d11d4ddd87005e2881433cc7707b)
- Use [terser](https://github.com/terser/terser) as js minifier, webui is no longer limited to ES5 feature set [cfd6e36d](https://github.com/xoseperez/espurna/commit/cfd6e36dbe94ee0e8098351357f903c060fd5dc9)
#### Settings
## [1.15.0-dev] Nightly build 2020-01-22
- Led and button GPIO runtime settings ([#2117](https://github.com/xoseperez/espurna/issues/2117), [#2162](https://github.com/xoseperez/espurna/issues/2162), [#2170](https://github.com/xoseperez/espurna/issues/2170), [#2177](https://github.com/xoseperez/espurna/issues/2177))
## [1.15.0-dev] Nightly build 2020-01-21
### System
- Refactor get/set/del/hasSetting ([#2048](https://github.com/xoseperez/espurna/issues/2048))
- Update migrate configuration & conditions, allow each module to access the current & previous version ([#2176](https://github.com/xoseperez/espurna/issues/2176))
#### Hardware
- lightfox relay provider & buttonAdd [bd3a5889](https://github.com/xoseperez/espurna/commit/bd3a588977fb8b195f2bba40618839b617767485)
- Fix GPIO16 support ([#2110](https://github.com/xoseperez/espurna/issues/2110), thanks to **[@foxman69](https://github.com/foxman69)**)
- Optionally preserve boot log in RAM ([#2109](https://github.com/xoseperez/espurna/issues/2109))
- Log modes, allow to disable debug messages on boot ([#2116](https://github.com/xoseperez/espurna/issues/2116))
### MQTT
- Set MQTT will topic after /get suffix initialization ([#2106](https://github.com/xoseperez/espurna/issues/2106), [#2115](https://github.com/xoseperez/espurna/issues/2115), thanks to **[@tomas-bara](https://github.com/tomas-bara)**)
### Hardware
- Added hardware config for Avatto NAS-WR01W ([#2113](https://github.com/xoseperez/espurna/issues/2113), thanks to **[@blockmar](https://github.com/blockmar)**)
### PlatformIO
- Deprecate -ota envs, handle OTA condition in extra script ([#2099](https://github.com/xoseperez/espurna/issues/2099))
## [1.15.0-dev] Nightly build 2020-01-14
### WebUI
- Fix scheduler panel tabindex= values ([#2096](https://github.com/xoseperez/espurna/issues/2096), thanks to **[@foxman69](https://github.com/foxman69)**)
### Hardware
- Add support for Kogan Smarter Home Plug With Energy Meter ([#2086](https://github.com/xoseperez/espurna/issues/2086), thanks to **[@aureq](https://github.com/aureq)**)
- Add support for Teckin SB53 smart bulb ([#2090](https://github.com/xoseperez/espurna/issues/2090), thanks to **[@marcuswinkler](https://github.com/marcuswinkler)**)
- Add Shelly 1PM GPIO picture ([#2092](https://github.com/xoseperez/espurna/issues/2092), thanks to **[@lblabr](https://github.com/lblabr)**)
- Add MagicHome ZJ\_LB\_RGBWW\_L support ([#2100](https://github.com/xoseperez/espurna/issues/2100), thanks to **[@wwilsman](https://github.com/wwilsman)**)
- Deltaco smart home devices ([#2103](https://github.com/xoseperez/espurna/issues/2103), thanks to **[@orrpan](https://github.com/orrpan)**)
## [1.14.1] 2019-12-31


+ 40
- 45
README.md View File

@ -6,10 +6,10 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
[![version](https://img.shields.io/badge/version-1.15.0--dev-brightgreen.svg)](CHANGELOG.md)
[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.com/xoseperez/espurna/tree/dev/)
[![license](https://img.shields.io/github/license/xoseperez/espurna.svg)](LICENSE)
[![ci build](https://github.com/xoseperez/espurna/workflows/ESPurna%20build/badge.svg?branch=dev)](https://github.com/xoseperez/espurna/actions)
[![ci build](https://github.com/xoseperez/espurna/actions/workflows/push.yml/badge.svg?branch=dev)](https://github.com/xoseperez/espurna/actions/workflows/push.yml)
<br />
[![latest master build](https://img.shields.io/github/release/xoseperez/espurna/all.svg?label=latest%20master%20build)](https://github.com/xoseperez/espurna/releases/latest)
[![latest dev build](https://img.shields.io/github/release/mcspr/espurna-nightly-builder/all.svg?label=latest%20dev%20build)](https://github.com/mcspr/espurna-nightly-builder/releases)
[![latest release](https://img.shields.io/github/v/release/xoseperez/espurna?label=latest%20release)](https://github.com/xoseperez/espurna/releases/latest)
[![latest snapshot build](https://img.shields.io/github/v/release/xoseperez/espurna?include_prereleases&label=latest%20snapshot%20build)](https://github.com/xoseperez/espurna/releases?q=prerelease%3Atrue&expanded=true)
[![downloads](https://img.shields.io/github/downloads/xoseperez/espurna/total.svg)](https://github.com/xoseperez/espurna/releases)
<br />
[![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=xose%2eperez%40gmail%2ecom&lc=US&no_note=0&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHostedGuest)
@ -257,19 +257,18 @@ For more information please refer to the [ESPurna Wiki](https://github.com/xosep
## Supported hardware
Here is the list of supported hardware. For more information please refer to the [ESPurna Wiki Hardware page](https://github.com/xoseperez/espurna/wiki/Hardware).
Here is the list of supported hardware. For more information please refer to the [ESPurna Wiki Hardware page](https://github.com/xoseperez/espurna/wiki/Hardware), search for wiki pages starting with *Hardware-...*, or search throught [our build configuration files with hardware presets](https://github.com/xoseperez/espurna/blob/dev/code/espurna/config/hardware.h).
### Power monitoring devices
||||
|---|---|---|
|![BlitzWolf BW-SHP6](images/devices/blitzwolf-bw-shp6.jpg)|![BlitzWolf BW-SHP2](images/devices/blitzwolf-bw-shp2.jpg)|![BlitzWolf BW-SHP5](images/devices/blitzwolf-bw-shp5.jpg)|
|**Blitzwolf BW-SHP6**|**Blitzwolf BW-SHP2<br />(also by Coosa, Gosund, HomeCube, Teckin)**|**Blitzwolf BW-SHP5**|
|**Blitzwolf BW-SHP6**|**[Blitzwolf BW-SHP2](https://github.com/xoseperez/espurna/wiki/Hardware-BLITZWOLF-BW-SHP2)**<br />(also by Coosa, Gosund, HomeCube, Teckin)|**Blitzwolf BW-SHP5**|
|![Power meters based on V9261F](images/devices/generic-v9261f.jpg)|![Itead Sonoff POW](images/devices/itead-sonoff-pow.jpg)|![Itead Sonoff POW](images/devices/itead-sonoff-pow-r2.jpg)|
|**Power meters based on V9261F**|**Itead Sonoff POW**|**Itead Sonoff POW R2**|
|**[Power meters based on V9261F](https://github.com/xoseperez/espurna/wiki/Hardware-Generic-V9261F)**|**[Itead Sonoff POW](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-POW)**|**[Itead Sonoff POW R2](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-POW-R2)**|
|![Itead Sonoff S31](images/devices/itead-sonoff-s31.jpg)|![Smartlife Mini Smart Socket](images/devices/smartlife-mini-smart-socket.jpg)|![Teckin SP20](images/devices/teckin-sp20.jpg)|
|**Itead Sonoff S31**|**Smartlife (NETVIP) Mini Smart Socket**|**Teckin SP20**|
|**[Itead Sonoff S31](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-S31)**|**Smartlife (NETVIP) Mini Smart Socket**|**Teckin SP20**|
|![Digoo NX SP202](images/devices/digoo-nx-sp202.jpg)|![Vanzavanzu Smart WiFi Plug Mini](images/devices/vanzavanzu-smart-wifi-plug-mini.jpg)|![Hykker Smart Home Power Plug](images/devices/hykker-smart-home-power-plug.jpg)|
|**Digoo NX SP202**|**Vanzavanzu Smart WiFi Plug Mini**|**Hykker Smart Home Power Plug**|
@ -278,15 +277,15 @@ Here is the list of supported hardware. For more information please refer to the
||||
|---|---|---|
|![Itead Sonoff Basic](images/devices/itead-sonoff-basic.jpg)|![Itead Sonoff Dual/Dual R2](images/devices/itead-sonoff-dual.jpg)|![Itead Sonoff TH10/TH16](images/devices/itead-sonoff-th.jpg)|
|**Itead Sonoff Basic (including R2 and R3)**|**Itead Sonoff Dual/Dual R2**|**Itead Sonoff TH10/TH16**|
|**[Itead Sonoff Basic (including R2 and R3)](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-Basic)**|**[Itead Sonoff Dual/Dual R2](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-Dual)**|**[Itead Sonoff TH10/TH16](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-TH)**|
|![Electrodragon WiFi IOT](images/devices/electrodragon-wifi-iot.jpg)|![OpenEnergyMonitor WiFi MQTT Relay / Thermostat](images/devices/openenergymonitor-mqtt-relay.jpg)||
|**Electrodragon WiFi IOT**|**OpenEnergyMonitor WiFi MQTT Relay / Thermostat**||
|**[Electrodragon WiFi IOT](https://github.com/xoseperez/espurna/wiki/Hardware-Electrodragon-ESP-Relay-Board)**|**[OpenEnergyMonitor WiFi MQTT Relay / Thermostat](https://github.com/xoseperez/espurna/wiki/Hardware-OpenEnergyMonitor-Wifi-MQTT-Relay)**||
|![Itead Sonoff 4CH](images/devices/itead-sonoff-4ch.jpg)|![Itead Sonoff 4CH Pro](images/devices/itead-sonoff-4ch-pro.jpg)||
|**Itead Sonoff 4CH**|**Itead Sonoff 4CH Pro**||
|**[Itead Sonoff 4CH](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-4CH)**|**[Itead Sonoff 4CH Pro](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-4CH-Pro---R2)**||
|![Allterco Shelly 1 / 1PM](images/devices/allterco-shelly1.jpg)|![Allterco Shelly 2 / 2.5](images/devices/allterco-shelly2.jpg)|![Jan Goedeke Wifi Relay (NO/NC)](images/devices/jangoe-wifi-relay.jpg)|
|**Alterco Shelly 1 / 1PM**|**Alterco Shelly 2 / 2.5**|**Jan Goedeke Wifi Relay (NO/NC)**|
|**Alterco Shelly 1 / 1PM**|**Alterco Shelly 2 / 2.5**|**[Jan Goedeke Wifi Relay (NO/NC)](https://github.com/xoseperez/espurna/wiki/Hardware-Jan-Goedeke-Wifi-Relay-Board)**|
|![EXS Wifi Relay v3.1](images/devices/exs-wifi-relay-v31.jpg)|![EXS Wifi Relay v5.0](images/devices/exs-wifi-relay-v50.jpg)|![Jorge García Wifi + Relays Board Kit](images/devices/jorgegarcia-wifi-relays.jpg)|
|**EXS Wifi Relay v3.1**|**EXS Wifi Relay v5.0**|**Jorge García Wifi + Relays Board Kit**|
|**[EXS Wifi Relay v3.1](https://github.com/xoseperez/espurna/wiki/Hardware-EXS-WiFi-Relay-v3.1)**|**EXS Wifi Relay v5.0**|**[Jorge García Wifi + Relays Board Kit](https://github.com/xoseperez/espurna/wiki/Hardware-Jorge-Garcia-Wifi-Relay-Board)**|
|![Allnet ESP8266-UP-Relay](images/devices/allnet-esp8266-up-relay.jpg)|![Bruno Horta's OnOfre](images/devices/bh-onofre.jpg)|![Luani HVIO](images/devices/luani-hvio.jpg)|
|**Allnet ESP8266-UP-Relay**|**Bruno Horta's OnOfre**|**Luani HVIO**|
@ -296,28 +295,28 @@ Here is the list of supported hardware. For more information please refer to the
||||
|---|---|---|
|![Itead S20](images/devices/itead-s20.jpg)|![Itead S26](images/devices/itead-s26.jpg)|![Neo Coolcam NAS WR01W](images/devices/neo-coolcam-wifi.jpg)|
|**Itead S20**|**Itead S26**|**Neo Coolcam NAS WR01W**|
|**[Itead S20](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-S20)**|**[Itead S26](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-S26)**|**[Neo Coolcam NAS WR01W](https://github.com/xoseperez/espurna/wiki/Hardware-NEO-COOLCAM-NAS-WR01W)**|
|![Maxcio W-US002S](images/devices/maxcio-w-us002s.jpg)|![HEYGO HY02](images/devices/heygo-hy02.jpg)|![YiDian XS-SSA05](images/devices/yidian-xs-ssa05.jpg)|
|**Maxcio W-US002S**|**HEYGO HY02**|**YiDian XS-SSA05**|
|**[Maxcio W-US002S](https://github.com/xoseperez/espurna/wiki/Hardware-Maxcio-W-US002S)**|**[HEYGO HY02](https://github.com/xoseperez/espurna/wiki/Hardware-HEYGO-HY02)**|**[YiDian XS-SSA05](https://github.com/xoseperez/espurna/wiki/Hardware-YiDian-XS-SSA05)**|
|![WiOn 50055](images/devices/wion-50055.jpg)|![LINGAN SWA1](images/devices/lingan-swa1.jpg)|![HomeCube 16A](images/devices/homecube-16a.jpg)|
|**WiOn 50055**|**LINGAN SWA1**|**HomeCube 16A**|
|**[WiOn 50055](https://github.com/xoseperez/espurna/wiki/Hardware-WiOn-50055)**|**[LINGAN SWA1](https://github.com/xoseperez/espurna/wiki/Hardware-LINGAN-SWA1)**|**HomeCube 16A**|
|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg)|![Bestek MRJ1011](images/devices/bestek-mrj1011.jpg)|![Tonbux XS-SSA01](images/devices/tonbux-xs-ssa01.jpg)|
|**WorkChoice EcoPlug**|**Bestek MRJ1011**|**Tonbux XS-SSA01**|
|**[WorkChoice EcoPlug](https://github.com/xoseperez/espurna/wiki/Hardware-WorkChoice-EcoPlug)**|**[Bestek MRJ1011](https://github.com/xoseperez/espurna/wiki/Hardware-Bestek-MRJ1011)**|**Tonbux XS-SSA01**|
|![Schuko Wifi Plug](images/devices/schuko-wifi-plug.jpg)|![Schuko Wifi Plug V2](images/devices/schuko-wifi-plug-v2.jpg)|![KMC 70011](images/devices/kmc-70011.jpg)|
|**Schuko Wifi Plug**|**Schuko Wifi Plug V2**|**KMC 70011**|
|**Schuko Wifi Plug**|**Schuko Wifi Plug V2**|**[KMC 70011](https://github.com/xoseperez/espurna/wiki/Hardware-KMC-70011)**|
|![Xenon SM-PW702U](images/devices/xenon-sm-pw702u.jpg)|![Orvibo B25](images/devices/orvibo-b25.jpg)|![Oukitel P1](images/devices/oukitel-p1.jpg)|
|**Xenon SM-PW702U**|**Orvibo B25**|**Oukitel P1**|
|**[Xenon SM-PW702U](https://github.com/xoseperez/espurna/wiki/Hardware-Xenon-SM-PW-702U)**|**[Orvibo B25](https://github.com/xoseperez/espurna/wiki/Hardware-Orvibo-B25)**|**Oukitel P1**|
|![Tonbux XS-SSA06](images/devices/tonbux-xs-ssa06.jpg)|![Litesun LA-WF3](images/devices/litesun-la-wf3.jpg)|![Maxcio W DE-004](images/devices/maxcio-w-de004.jpg)|
|**Tonbux XS-SSA06**|**Litesun LA-WF3**|**Maxcio W DE-004**|
|**[Tonbux XS-SSA06](https://github.com/xoseperez/espurna/wiki/Hardware-Tonbux-XS-SSA06)**|**Litesun LA-WF3**|**[Maxcio W DE-004](https://github.com/xoseperez/espurna/wiki/Hardware-Maxcio-W-DE004)**|
|![Hama WiFi Steckdose](images/devices/hama-wifi-steckdose.jpg)|![GBLife RGBW Socket](images/devices/gblife-rgbw-socket.jpg)||
|**Hama WiFi Steckdose**|**GBLife RGBW Socket**||
|**[Hama WiFi Steckdose](https://github.com/xoseperez/espurna/wiki/Hardware-Hama-WiFi-Steckdose,-3.500W,-16A-(Article-Number-00176533))**|**GBLife RGBW Socket**||
### Wall switches
||||
|---|---|---|
|![Itead Sonoff Touch](images/devices/itead-sonoff-touch.jpg)|![Itead Sonoff T1](images/devices/itead-sonoff-t1.jpg)|![YJZK switch](images/devices/yjzk-2gang-switch.jpg)|
|**Itead Sonoff Touch**|**Itead Sonoff T1**|**YJZK 1/2/3-gangs switch**|
|**[Itead Sonoff Touch](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-Touch)**|**[Itead Sonoff T1](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-T1)**|**YJZK 1/2/3-gangs switch**|
|![Gosund WS1 / KS-602S](images/devices/gosund-ws1.jpg)|||
|**Gosund WS1 / KS-602S**|||
@ -325,67 +324,63 @@ Here is the list of supported hardware. For more information please refer to the
||||
|---|---|---|
|![Tonbux PowerStrip02](images/devices/tonbux-powerstrip02.jpg)|![ForNorm ZLD-34EU](images/devices/fornorm-power-strip.jpg)|![Zhilde ZLD-EU55-W](images/devices/zhilde-zld-eu55-w.jpg)|
|**Tonbux PowerStrip02**|**Fornorm Power Strip**|**Zhilde ZLD-EU55-W**|
|![Tonbux PowerStrip02](images/devices/tonbux-powerstrip02.jpg)|![ForNorm ZLD-34EU](images/devices/fornorm-power-strip.jpg)|![Zhilde ZLD-EU44-W](images/devices/zhilde-zld-eu55-w.jpg)|
|**[Tonbux PowerStrip02](https://github.com/xoseperez/espurna/wiki/Hardware-Tonbux-Powerstrip02)**|**Fornorm Power Strip**|**[Zhilde ZLD-44EU-W/ZLD-64EU-W](https://github.com/xoseperez/espurna/wiki/Hardware-Zhilde-ZLD-64EU-W)**|
### Smart lights
||||
|---|---|---|
|![Itead Slampher](images/devices/itead-slampher.jpg)|![Arilux E27](images/devices/arilux-e27.jpg)|![Itead Sonoff B1](images/devices/itead-sonoff-b1.jpg)|
|**Itead Slampher**|**Arilux E27**|**Itead Sonoff B1**|
|**[Itead Slampher](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Slampher)**|**[Arilux E27](https://github.com/xoseperez/espurna/wiki/Hardware-Arilux-E27)**|**[Itead Sonoff B1](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-B1)**|
|![AI-Thinker Wifi Light / Noduino OpenLight](images/devices/aithinker-ai-light.jpg)|![Authometion LYT8266](images/devices/authometion-lyt8266.jpg)|![AG-L4](images/devices/ag-l4.jpg)|
|**AI-Thinker Wifi Light / Noduino OpenLight**|**Authometion LYT8266**|**AG-L4**|
|**[AI-Thinker Wifi Light / Noduino OpenLight](https://github.com/xoseperez/espurna/wiki/Hardware-AI-Thinker-AI-Light)**|**Authometion LYT8266**|**AG-L4**|
|![Lohas 9W](images/devices/lohas-9w.jpg)|![Xiaomi Smart Desk Lamp](images/devices/xiaomi-smart-desk-lamp.jpg)|![iWoole LED Table Lamp](images/devices/iwoole-led-table-lamp.jpg)|
|**Lohas 9W**|**Xiaomi Smart Desk Lamp**|**iWoole LED Table Lamp**|
|**Lohas 9W**|**Xiaomi Smart Desk Lamp**|**[iWoole LED Table Lamp](https://github.com/xoseperez/espurna/wiki/Hardware-IWOOLE_LED_TABLE_LAMP)**|
|![Itead Sonoff LED](images/devices/itead-sonoff-led.jpg)|![Itead BN-SZ01](images/devices/itead-bn-sz01.jpg)|![Lombox LUX Nova 2](images/devices/lombex-lux-nova2.jpg)|
|**Itead Sonoff LED**|**Itead BN-SZ01**|**Lombex LUX Nova 2 (white and color)**|
|**[Itead Sonoff LED](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-LED)**|**[Itead BN-SZ01](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-BN-SZ01)**|**[Lombex LUX Nova 2 (white and color)](https://github.com/xoseperez/espurna/wiki/Hardware-Lombex-Lux-Nova)**|
|![Arilux AL-LC01 (RGB)](images/devices/arilux-al-lc01.jpg)|![Arilux AL-LC02 (RGBW)](images/devices/arilux-al-lc02.jpg)|![Arilux AL-LC06 (RGBWWCW)](images/devices/arilux-al-lc06.jpg)|
|**Arilux AL-LC01 (RGB)**|**Arilux AL-LC02 (RGBW)**|**Arilux AL-LC06 (RGBWWCW)**|
|**Arilux AL-LC01 (RGB)**|**Arilux AL-LC02 (RGBW)**|**[Arilux AL-LC06 (RGBWWCW)](https://github.com/xoseperez/espurna/wiki/Hardware-Arilux-AL-LC06)**|
|![Arilux AL-LC11 (RGBWWW) & RF](images/devices/arilux-al-lc11.jpg)|![MagicHome LED Controller (1.0 and 2.x)](images/devices/magichome-led-controller.jpg)|![Huacanxing H801/802](images/devices/huacanxing-h801.jpg)|
|**Arilux AL-LC11 (RGBWWW) & RF**|**MagicHome LED Controller (1.0/2.x, also ZJ WFMN A/B11)**|**Huacanxing H801/802**|
|![Muvit I/O MIOBULB001](images/devices/muvit-io-miobulb001.jpg)|
|**Muvit I/O MIOBULB001**|
|**Arilux AL-LC11 (RGBWWW) & RF**|**[MagicHome LED Controller (1.0/2.x, also ZJ WFMN A/B11)](https://github.com/xoseperez/espurna/wiki/Hardware-Magic-Home-LED-Controller)**|**[Huacanxing H801/802](https://github.com/xoseperez/espurna/wiki/Hardware-Huacanxing-H80x)**|
|![Muvit I/O MIOBULB001](images/devices/muvit-io-miobulb001.jpg)|![LAMPI Smart Light](images/devices/lampi-rgbww-smart-lamp.jpg)
|**[Muvit I/O MIOBULB001](https://github.com/xoseperez/espurna/wiki/Hardware-Muvit-IO-miobulb001)**|**[Lampi Battery Powered Smart Light](https://github.com/xoseperez/espurna/wiki/Hardware-Lampi-RGBWW-Battery-Powered-Smart-Light)**|
### Radio links / gateways
||||
|---|---|---|
|![Tinkerman RFM69GW](images/devices/tinkerman-rfm69gw.jpg)|![Itead Sonoff RF Bridge](images/devices/itead-sonoff-rfbridge.jpg)|![Itead Sonoff RF](images/devices/itead-sonoff-rf.jpg)|
|**Tinkerman RFM69GW**|**Itead Sonoff RF Bridge**|**Itead Sonoff RF**|
|**Tinkerman RFM69GW**|**[Itead Sonoff RF Bridge](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-RF-Bridge)**|**[Itead Sonoff RF](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-RF)**|
### Other devices
||||
|---|---|---|
|![Tonbux Mosquito Killer](images/devices/tonbux-mosquito-killer.jpg)|![Itead Sonoff IFAN02](images/devices/itead-sonoff-ifan02.jpg)||
|**Tonbux Mosquito Killer**|**Itead Sonoff IFAN02**|||
|**[Tonbux Mosquito Killer](https://github.com/xoseperez/espurna/wiki/Hardware-Tonbux-Mosquito-Killer)**|**Itead Sonoff IFAN02**|||
### Custom & Development boards
||||
|---|---|---|
|![Tinkerman Espurna H](images/devices/tinkerman-espurna-h.jpg)||![NodeMCU](images/devices/nodemcu-lolin-v3.jpg)|
|**Tinkerman ESPurna H**||**NodeMCU Lolin V3**|
|**[Tinkerman ESPurna H](https://github.com/xoseperez/espurna/wiki/Hardware-Tinkerman-ESPurna-H)**||**[NodeMCU Lolin V3](https://github.com/xoseperez/espurna/wiki/Hardware-NodeMCU-Lolin)**|
|![Itead Sonoff SV](images/devices/itead-sonoff-sv.jpg)|![Itead 1CH Inching](images/devices/itead-1ch-inching.jpg)|![Itead Motor Clockwise/Anticlockwise](images/devices/itead-motor.jpg)|
|**Itead Sonoff SV**|**Itead 1CH Inching**|**Itead Motor Clockwise/Anticlockwise**|
|**[Itead Sonoff SV](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-SV)**|**[Itead 1CH Inching](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-1CH)**|**[Itead Motor Clockwise/Anticlockwise](https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Motor)**|
|![ManCaveMade ESP-Live](images/devices/mancavemade-esp-live.jpg)|![Wemos D1 Mini Relay Shield](images/devices/wemos-d1-relayshield.jpg)|![Gizwits Witty Cloud](images/devices/witty-cloud.jpg)|
|**ManCaveMade ESP-Live**|**Wemos D1 Mini Relay Shield**|**Gizwits Witty Cloud**|
|**[ManCaveMade ESP-Live](https://github.com/xoseperez/espurna/wiki/Hardware-ManCaveMade-ESPLive)**|**[Wemos D1 Mini Relay Shield](https://github.com/xoseperez/espurna/wiki/Hardware-Wemos-D1-Mini-Relay-Shield)**|**[Gizwits Witty Cloud](https://github.com/xoseperez/espurna/wiki/Hardware-Witty-Cloud)**|
|![IKE ESPike](images/devices/ike-espike.jpg)|![Pilotak ESP DIN](images/devices/pilotak-esp-din.jpg)|![Arniex Swifitch](images/devices/arniex-swifitch.jpg)|
|**IKE ESPike**|**Pilotak ESP DIN**|**Arniex Swifitch**|
|**[IKE ESPike](https://github.com/xoseperez/espurna/wiki/Hardware-IKE-ESPike)**|**Pilotak ESP DIN**|**[Arniex Swifitch](https://github.com/xoseperez/espurna/wiki/Hardware-Arniex-Swifitch)**|
|![Heltec Touch Relay](images/devices/heltec-touch-relay.jpg)|![Generic Relay v4.0](images/devices/generic-relay-40.jpg)|![Generic RGBLed v1.0](images/devices/generic-rgbled-10.jpg)|
|**Heltec Touch Relay**|**Generic Relay v4.0**|**Generic RGBLed v1.0**|
|**[Heltec Touch Relay](https://github.com/xoseperez/espurna/wiki/Hardware-Heltec-Touch-Relay)**|**[Generic Relay v4.0](https://github.com/xoseperez/espurna/wiki/Hardware-Generic-Relay-v40)**|**[Generic RGBLed v1.0](https://github.com/xoseperez/espurna/wiki/Hardware-Generic-RGBLed-v10)**|
|![Generic DHT11 v1.0](images/devices/generic-dht11-10.jpg)|![Generic DS18B20 v1.0](images/devices/generic-ds18b20-10.jpg)|![InterMitTech QuinLED 2.6](images/devices/intermittech-quinled-2.6.jpg)|
|**Generic DHT11 v1.0**|**Generic DS18B20 v1.0**|**InterMitTech QuinLED 2.6**|
|**[Generic DHT11 v1.0](https://github.com/xoseperez/espurna/wiki/Hardware-Generic-DHT11-v10)**|**[Generic DS18B20 v1.0](https://github.com/xoseperez/espurna/wiki/Hardware-Generic-DS18B20-v10)**|**[InterMitTech QuinLED 2.6](https://github.com/xoseperez/espurna/wiki/Hardware-QuinLED)**|
|![Phyx ESP12 RGBW](images/devices/phyx-esp12-rgbw.jpg)|![RH Electronics Geiger Counter](images/devices/generic-geiger-diy.png)|![Green ESP Relay](images/devices/green-esp-relay.jpg)|
|**Phyx ESP12 RGBW**|**RH Electronics Geiger Counter**|**Green ESP Relay**|
|**Phyx ESP12 RGBW**|**[RH Electronics Geiger Counter](https://github.com/xoseperez/espurna/wiki/Geiger-Counter)**|**[Green ESP Relay](https://github.com/xoseperez/espurna/wiki/Hardware-Green-ESP8266-Relay)**|
|![Foxel Lightfox Dual](images/devices/foxel-lightfox-dual.jpg)|||
|**Foxel Lightfox Dual**|||
**Other supported boards (beta):**
KMC 4 Outlet, Gosund WS1, MakerFocus Intelligent Module LM33 for Lamps
## License
Copyright (C) 2016-2019 by Xose Pérez (@xoseperez)


+ 1
- 2
ci_install.sh View File

@ -14,14 +14,13 @@ pio_install() {
}
host_install() {
pio platform install native
sudo apt install cmake
}
cd code
case "$1" in
("host")
pio_install
host_install
;;
("webui")


+ 17
- 6
ci_script.sh View File

@ -6,21 +6,32 @@ cd code
case "$1" in
("host")
# runs PIO unit tests, using the host compiler
# (see https://github.com/ThrowTheSwitch/Unity)
pushd test
pio test
# runs unit tests, using the host compiler and the esp8266 mock framework
# - https://github.com/esp8266/Arduino/blob/master/tests/host/Makefile
# - https://github.com/ThrowTheSwitch/Unity
pushd test/unit
cmake -B build
cmake --build build
cmake --build build --target test
popd
;;
("webui")
# TODO: both can only parse one file at a time
npm exec --no eslint html/custom.js
npm exec --no html-validate html/index.html
npm exec --no -- eslint html/custom.js
npm exec --no -- html-validate html/index.html
# checks whether the webui can be built
./build.sh -f environments
# TODO: gzip inserts an OS-dependant byte in the header, ref.
# - https://datatracker.ietf.org/doc/html/rfc1952
# - https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/deps/zlib/deflate.c#L901
# - windowBits description in the https://zlib.net/manual.html#Advanced
git --no-pager diff --stat
;;
("build")
# simply build the given environment
pio run -e $2
;;
("test")
# run generic build test with the specified environment as base
scripts/test_build.py -e $2
;;


+ 0
- 14
code/.gitignore View File

@ -1,14 +0,0 @@
.clang_complete
core_version.h
custom.h
.DS_Store
.gcc-flags.json
.python-version
.travis.yml
.vscode
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.pio
libraries/
espurna/espurna.ino.cpp

+ 2
- 1
code/.htmlvalidate.json View File

@ -7,6 +7,7 @@
],
"rules": {
"wcag/h32": "off",
"wcag/h71": "off"
"wcag/h71": "off",
"valid-id": ["error", {"relaxed": true}]
}
}

+ 121
- 86
code/espurna/alexa.cpp View File

@ -6,12 +6,13 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "alexa.h"
#include "espurna.h"
#if ALEXA_SUPPORT
#include <queue>
#include "alexa.h"
#include "api.h"
#include "light.h"
#include "mqtt.h"
@ -23,11 +24,13 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <fauxmoESP.h>
#include <ArduinoJson.h>
namespace espurna {
namespace alexa {
namespace {
struct AlexaEvent {
AlexaEvent() = delete;
AlexaEvent(unsigned char id, bool state, unsigned char value) :
struct Event {
Event() = delete;
Event(unsigned char id, bool state, unsigned char value) :
_id(id),
_state(state),
_value(value)
@ -51,22 +54,23 @@ private:
unsigned char _value;
};
std::queue<AlexaEvent> _alexa_events;
fauxmoESP _alexa;
std::queue<Event> events;
fauxmoESP fauxmo;
namespace alexa {
namespace build {
constexpr bool createServer() {
return !WEB_SUPPORT;
return WEB_SUPPORT == 0;
}
constexpr uint16_t port() {
return 80;
}
const __FlashStringHelper* hostname() {
return F(ALEXA_HOSTNAME);
PROGMEM_STRING(Hostname, ALEXA_HOSTNAME);
constexpr espurna::StringView hostname() {
return Hostname;
}
constexpr bool enabled() {
@ -74,86 +78,117 @@ constexpr bool enabled() {
}
} // namespace build
namespace settings {
namespace keys {
PROGMEM_STRING(Enabled, "alexaEnabled");
PROGMEM_STRING(Name, "alexaName");
} // namespace keys
bool enabled() {
return getSetting("alexaEnabled", build::enabled());
return getSetting(keys::Enabled, build::enabled());
}
// Use custom alexa hostname if defined, device hostname otherwise
String hostname() {
auto out = getSetting("alexaName", build::hostname());
auto out = getSetting(keys::Name, build::hostname());
if (!out.length()) {
out = getSetting("hostname", getIdentifier());
out = systemHostname();
}
return out;
}
} // namespace settings
} // namespace alexa
void _alexaSettingsMigrate(int version) {
if (version && (version < 3)) {
moveSetting("fauxmoEnabled", "alexaEnabled");
void migrate(int version) {
if (version < 3) {
moveSetting(PSTR("fauxmoEnabled"), keys::Enabled);
}
}
// -----------------------------------------------------------------------------
// ALEXA
// -----------------------------------------------------------------------------
} // namespace settings
bool _alexaWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "alexa", 5) == 0);
#if WEB_SUPPORT
namespace web {
PROGMEM_STRING(Prefix, "alexa");
void onVisible(JsonObject& root) {
wsPayloadModule(root, Prefix);
}
void _alexaWebSocketOnConnected(JsonObject& root) {
root["alexaEnabled"] = alexa::settings::enabled();
root["alexaName"] = alexa::settings::hostname();
bool onKeyCheck(StringView key, const JsonVariant&) {
return key.startsWith(Prefix);
}
void _alexaConfigure() {
_alexa.enable(wifiConnected() && alexa::settings::enabled());
void onConnected(JsonObject& root) {
root[FPSTR(settings::keys::Enabled)] = alexa::settings::enabled();
root[FPSTR(settings::keys::Name)] = alexa::settings::hostname();
}
#if WEB_SUPPORT
bool _alexaBodyCallback(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
return _alexa.process(request->client(), request->method() == HTTP_GET, request->url(), String((char *)data));
bool body_callback(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (len != total) {
DEBUG_MSG_P(PSTR("[ALEXA] Ignoring incomplete %s %s from %s (%zu / %zu)\n"),
request->methodToString(),
request->url().c_str(),
IPAddress(request->client()->getRemoteAddress()).toString().c_str(),
len, index);
return false;
}
bool _alexaRequestCallback(AsyncWebServerRequest *request) {
String body = (request->hasParam("body", true)) ? request->getParam("body", true)->value() : String();
return _alexa.process(request->client(), request->method() == HTTP_GET, request->url(), body);
}
String payload;
payload.concat(reinterpret_cast<char*>(data), len);
return fauxmo.process(request->client(), request->method() == HTTP_GET, request->url(), payload);
}
bool request_callback(AsyncWebServerRequest *request) {
String body = (request->hasParam("body", true)) ? request->getParam("body", true)->value() : String();
return fauxmo.process(request->client(), request->method() == HTTP_GET, request->url(), body);
}
void setup() {
webBodyRegister(body_callback);
webRequestRegister(request_callback);
wsRegister()
.onVisible(onVisible)
.onConnected(onConnected)
.onKeyCheck(onKeyCheck);
}
} // namespace web
#endif
void configure() {
fauxmo.enable(wifiConnected() && alexa::settings::enabled());
}
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
void _alexaUpdateLights() {
_alexa.setState(static_cast<unsigned char>(0u), lightState(), lightState() ? 255u : 0u);
void update() {
fauxmo.setState(uint8_t{ 0 }, lightState(), lightState() ? 255u : 0u);
auto channels = lightChannels();
for (decltype(channels) channel = 0; channel < channels; ++channel) {
auto value = lightChannel(channel);
_alexa.setState(channel + 1, value > 0, value);
fauxmo.setState(channel + 1, value > 0, value);
}
}
#endif
#if RELAY_SUPPORT
#elif RELAY_SUPPORT
void _alexaUpdateRelay(size_t id, bool status) {
_alexa.setState(id, status, status ? 255 : 0);
void update(size_t id, bool status) {
fauxmo.setState(id, status, status ? 255 : 0);
}
#endif
void _alexaLoop() {
_alexa.handle();
void loop() {
fauxmo.handle();
while (!_alexa_events.empty()) {
auto& event = _alexa_events.front();
while (!events.empty()) {
auto& event = events.front();
DEBUG_MSG_P(PSTR("[ALEXA] Device #%hhu state=#%c value=%hhu\n"),
event.id(), event.state() ? 't' : 'f', event.value());
@ -169,25 +204,17 @@ void _alexaLoop() {
relayStatus(event.id(), event.state());
#endif
_alexa_events.pop();
events.pop();
}
}
} // namespace
// -----------------------------------------------------------------------------
bool alexaEnabled() {
return alexa::settings::enabled();
}
void alexaSetup() {
void setup() {
// Backwards compatibility
_alexaSettingsMigrate(migrateVersion());
migrateVersion(settings::migrate);
// Basic fauxmoESP configuration
_alexa.createServer(alexa::build::createServer());
_alexa.setPort(alexa::build::port());
fauxmo.createServer(alexa::build::createServer());
fauxmo.setPort(alexa::build::port());
auto hostname = alexa::settings::hostname();
auto deviceName = [&](size_t index) {
@ -199,9 +226,9 @@ void alexaSetup() {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
// 1st is the global state, the rest are mapped to channel values
_alexa.addDevice(hostname.c_str());
fauxmo.addDevice(hostname.c_str());
for (size_t channel = 1; channel <= lightChannels(); ++channel) {
_alexa.addDevice(deviceName(channel).c_str());
fauxmo.addDevice(deviceName(channel).c_str());
}
// Relays are mapped 1-to-1
@ -209,46 +236,54 @@ void alexaSetup() {
auto relays = relayCount();
if (relays > 1) {
for (decltype(relays) id = 1; id <= relays; ++id) {
_alexa.addDevice(deviceName(id).c_str());
fauxmo.addDevice(deviceName(id).c_str());
}
} else {
_alexa.addDevice(hostname.c_str());
fauxmo.addDevice(hostname.c_str());
}
#endif
// Websockets
#if WEB_SUPPORT
webBodyRegister(_alexaBodyCallback);
webRequestRegister(_alexaRequestCallback);
wsRegister()
.onVisible([](JsonObject& root) { root["alexaVisible"] = 1; })
.onConnected(_alexaWebSocketOnConnected)
.onKeyCheck(_alexaWebSocketOnKeyCheck);
#endif
#if WEB_SUPPORT
web::setup();
#endif
// Register wifi callback
wifiRegister([](wifi::Event event) {
if ((event == wifi::Event::StationConnected)
|| (event == wifi::Event::StationDisconnected)) {
_alexaConfigure();
wifiRegister([](espurna::wifi::Event event) {
switch (event) {
case espurna::wifi::Event::StationConnected:
case espurna::wifi::Event::StationDisconnected:
configure();
default:
break;
}
});
// Callback
_alexa.onSetState([&](unsigned char device_id, const char*, bool state, unsigned char value) {
_alexa_events.emplace(device_id, state, value);
fauxmo.onSetState([](unsigned char device_id, const char*, bool state, unsigned char value) {
events.emplace(device_id, state, value);
});
// Register main callbacks
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
lightSetReportListener(_alexaUpdateLights);
#else
relaySetStatusChange(_alexaUpdateRelay);
lightOnReport(update);
#elif RELAY_SUPPORT
relayOnStatusChange(update);
#endif
espurnaRegisterReload(_alexaConfigure);
espurnaRegisterLoop(_alexaLoop);
espurnaRegisterReload(configure);
espurnaRegisterLoop(loop);
}
} // namespace
} // namespace alexa
} // namespace espurna
bool alexaEnabled() {
return espurna::alexa::settings::enabled();
}
void alexaSetup() {
espurna::alexa::setup();
}
#endif

+ 0
- 2
code/espurna/alexa.h View File

@ -8,7 +8,5 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#pragma once
#include "espurna.h"
bool alexaEnabled();
void alexaSetup();

+ 281
- 208
code/espurna/api.cpp View File

@ -7,17 +7,19 @@ Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
*/
#include "api.h"
// -----------------------------------------------------------------------------
#include "system.h"
#include "rpc.h"
#include "espurna.h"
#if API_SUPPORT
#include "api.h"
#endif
#if WEB_SUPPORT
#include "web.h"
#include <ESPAsyncTCP.h>
#include <ArduinoJson.h>
#include "web.h"
#endif
#include <algorithm>
@ -26,9 +28,14 @@ Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#include <forward_list>
#include <vector>
#include "system.h"
#include "rpc.h"
#include "api_path.h"
// -----------------------------------------------------------------------------
PathParts::PathParts(const String& path) :
PathParts::PathParts(espurna::StringView path) :
_path(path)
{
if (!_path.length()) {
@ -40,7 +47,7 @@ PathParts::PathParts(const String& path) :
size_t length { 0ul };
size_t offset { 0ul };
const char* p { _path.c_str() };
const char* p { _path.begin() };
if (*p == '\0') {
goto error;
}
@ -193,81 +200,152 @@ error:
return false;
}
String ApiRequest::wildcard(int index) const {
espurna::StringView PathParts::wildcard(const PathParts& pattern, const PathParts& value, int index) {
if (index < 0) {
index = std::abs(index + 1);
}
if (std::abs(index) >= _pattern.parts().size()) {
return _empty_string();
}
espurna::StringView out;
int counter { 0 };
auto& pattern = _pattern.parts();
if (std::abs(index) < pattern.parts().size()) {
const auto& pattern_parts = pattern.parts();
int counter { 0 };
for (unsigned int part = 0; part < pattern.size(); ++part) {
auto& lhs = pattern[part];
if (PathPart::Type::SingleWildcard == lhs.type) {
if (counter == index) {
auto& rhs = _parts.parts()[part];
return _parts.path().substring(rhs.offset, rhs.offset + rhs.length);
for (size_t part = 0; part < pattern.size(); ++part) {
const auto& lhs = pattern_parts[part];
const auto& rhs = value.parts()[part];
const auto path = value.path();
switch (lhs.type) {
case PathPart::Type::Value:
case PathPart::Type::Unknown:
break;
case PathPart::Type::SingleWildcard:
if (counter == index) {
out = espurna::StringView(
path.begin() + rhs.offset, path.begin() + rhs.offset + rhs.length);
return out;
}
++counter;
break;
case PathPart::Type::MultiWildcard:
if (counter == index) {
out = espurna::StringView(
path.begin() + rhs.offset, path.end());
}
return out;
}
++counter;
}
}
return _empty_string();
return out;
}
size_t ApiRequest::wildcards() const {
size_t result { 0ul };
for (auto& part : _pattern) {
if (PathPart::Type::SingleWildcard == part.type) {
++result;
size_t PathParts::wildcards(const PathParts& pattern) {
size_t out { 0 };
for (const auto& part : pattern) {
switch (part.type) {
case PathPart::Type::Unknown:
case PathPart::Type::Value:
case PathPart::Type::MultiWildcard:
break;
case PathPart::Type::SingleWildcard:
++out;
break;
}
}
return result;
return out;
}
#if WEB_SUPPORT
String ApiRequest::wildcard(int index) const {
return PathParts::wildcard(_pattern, _parts, index).toString();
}
size_t ApiRequest::wildcards() const {
return PathParts::wildcards(_pattern);
}
#endif
// -----------------------------------------------------------------------------
#if API_SUPPORT
namespace espurna {
namespace api {
namespace content_type {
namespace {
STRING_VIEW_INLINE(Anything, "*/*");
STRING_VIEW_INLINE(Text, "text/plain");
STRING_VIEW_INLINE(Json, "application/json");
STRING_VIEW_INLINE(Form, "application/x-www-form-urlencoded");
} // namespace
} // namespace content_type
bool _apiAccepts(AsyncWebServerRequest* request, const __FlashStringHelper* str) {
auto* header = request->getHeader(F("Accept"));
if (header) {
return
(header->value().indexOf(F("*/*")) >= 0)
|| (header->value().indexOf(str) >= 0);
StringView Request::param(const String& name) {
const auto* result = _request.getParam(name, HTTP_PUT == _request.method());
espurna::StringView out;
if (result) {
out = result->value();
}
return false;
return out;
}
bool _apiAcceptsText(AsyncWebServerRequest* request) {
return _apiAccepts(request, F("text/plain"));
}
void Request::send(const String& payload) {
if (_done) {
return;
}
bool _apiAcceptsJson(AsyncWebServerRequest* request) {
return _apiAccepts(request, F("application/json"));
_done = true;
if (payload.length()) {
_request.send(200,
content_type::Text.toString(),
payload);
} else {
_request.send(204);
}
}
bool _apiMatchHeader(AsyncWebServerRequest* request, const __FlashStringHelper* key, const __FlashStringHelper* value) {
auto* header = request->getHeader(key);
if (header) {
return header->value().equals(value);
namespace {
bool accepts(AsyncWebServerRequest* request, StringView pattern) {
STRING_VIEW_INLINE(Accept, "Accept");
auto* header = request->getHeader(Accept.toString());
if (!header) {
return true;
}
return false;
return (header->value().indexOf(
StringView(content_type::Anything).toString()) >= 0)
|| (header->value().indexOf(pattern.toString()) >= 0);
}
bool accepts_text(AsyncWebServerRequest* request) {
return accepts(request, content_type::Text);
}
bool accepts_json(AsyncWebServerRequest* request) {
return accepts(request, content_type::Json);
}
bool is_content_type(AsyncWebServerRequest* request, StringView value) {
return value == request->contentType();
}
bool _apiIsJsonContent(AsyncWebServerRequest* request) {
return _apiMatchHeader(request, F("Content-Type"), F("application/json"));
bool is_form_data(AsyncWebServerRequest* request) {
return is_content_type(request, content_type::Form);
}
bool _apiIsFormDataContent(AsyncWebServerRequest* request) {
return _apiMatchHeader(request, F("Content-Type"), F("application/x-www-form-urlencoded"));
bool is_json(AsyncWebServerRequest* request) {
return is_content_type(request, content_type::Json);
}
// Because the webserver request is split between multiple separate function invocations, we need to preserve some state.
@ -286,27 +364,36 @@ bool _apiIsFormDataContent(AsyncWebServerRequest* request) {
// - ALL headers are parsed (and we could access those during filter and canHandle callbacks), but we need to explicitly
// request them to stay in memory so that the actual handler can work with them
void _apiAttachHelper(AsyncWebServerRequest& request, ApiRequestHelper&& helper) {
request._tempObject = new ApiRequestHelper(std::move(helper));
request.onDisconnect([&]() {
auto* ptr = reinterpret_cast<ApiRequestHelper*>(request._tempObject);
delete ptr;
request._tempObject = nullptr;
});
request.addInterestingHeader(F("Api-Key"));
void attach_helper(AsyncWebServerRequest& request, RequestHelper&& helper) {
request._tempObject = new RequestHelper(std::move(helper));
request.onDisconnect(
[&]() {
auto* ptr = reinterpret_cast<RequestHelper*>(request._tempObject);
delete ptr;
request._tempObject = nullptr;
});
request.addInterestingHeader(
STRING_VIEW("Api-Key").toString());
request.addInterestingHeader(
STRING_VIEW("Accept").toString());
}
class ApiBaseWebHandler : public AsyncWebHandler {
class BaseWebHandler : public AsyncWebHandler {
public:
ApiBaseWebHandler() = delete;
ApiBaseWebHandler(const ApiBaseWebHandler&) = delete;
ApiBaseWebHandler(ApiBaseWebHandler&&) = delete;
BaseWebHandler() = delete;
// In case this needs to be copied or moved, ensure PathParts copy references the new object's string
BaseWebHandler(const BaseWebHandler&) = delete;
BaseWebHandler& operator=(const BaseWebHandler&) = delete;
BaseWebHandler(BaseWebHandler&&) = delete;
BaseWebHandler& operator=(BaseWebHandler&&) = delete;
template <typename Pattern>
explicit ApiBaseWebHandler(Pattern&& pattern) :
_pattern(std::forward<Pattern>(pattern)),
// In case this needs to be copied or moved, ensure PathParts copy references the new object's string
template <typename T,
typename = typename std::enable_if<
std::is_constructible<String, T>::value>::type>
explicit BaseWebHandler(T&& pattern) :
_pattern(std::forward<T>(pattern)),
_parts(_pattern)
{}
@ -333,78 +420,23 @@ private:
// TODO: somehow detect partial data and buffer (optionally)
// TODO: POST instead of PUT?
class ApiJsonWebHandler final : public ApiBaseWebHandler {
class JsonWebHandler final : public BaseWebHandler {
public:
static constexpr size_t BufferSize { API_JSON_BUFFER_SIZE };
struct ReadOnlyStream : public Stream {
ReadOnlyStream() = delete;
explicit ReadOnlyStream(const uint8_t* buffer, size_t size) :
_buffer(buffer),
_size(size)
{}
int available() override {
return _size - _index;
}
JsonWebHandler() = delete;
int peek() override {
if (_index < _size) {
return static_cast<int>(_buffer[_index]);
}
JsonWebHandler(const JsonWebHandler&) = delete;
JsonWebHandler& operator=(const JsonWebHandler&) = delete;
return -1;
}
JsonWebHandler(JsonWebHandler&&) = delete;
JsonWebHandler& operator=(JsonWebHandler&&) = delete;
int read() override {
auto peeked = peek();
if (peeked >= 0) {
++_index;
}
return peeked;
}
// since we are fixed in size, no need for any timeouts and the only available option is to return full chunk of data
size_t readBytes(uint8_t* ptr, size_t size) override {
if ((_index < _size) && ((_size - _index) >= size)) {
std::copy(_buffer + _index, _buffer + _index + size, ptr);
_index += size;
return size;
}
return 0;
}
size_t readBytes(char* ptr, size_t size) override {
return readBytes(reinterpret_cast<uint8_t*>(ptr), size);
}
void flush() override {
}
size_t write(const uint8_t*, size_t) override {
return 0;
}
size_t write(uint8_t) override {
return 0;
}
const uint8_t* _buffer;
const size_t _size;
size_t _index { 0 };
};
ApiJsonWebHandler() = delete;
ApiJsonWebHandler(const ApiJsonWebHandler&) = delete;
ApiJsonWebHandler(ApiJsonWebHandler&&) = delete;
template <typename Path, typename Callback>
ApiJsonWebHandler(Path&& path, Callback&& get, Callback&& put) :
ApiBaseWebHandler(std::forward<Path>(path)),
_get(std::forward<Callback>(get)),
_put(std::forward<Callback>(put))
template <typename Path, typename Get, typename Put>
JsonWebHandler(Path&& path, Get&& get, Put&& put) :
BaseWebHandler(std::forward<Path>(path)),
_get(std::forward<Get>(get)),
_put(std::forward<Put>(put))
{}
bool isRequestHandlerTrivial() override {
@ -416,22 +448,19 @@ public:
return false;
}
if (!_apiAcceptsJson(request)) {
return false;
}
auto helper = ApiRequestHelper(*request, parts());
auto helper = RequestHelper(*request, parts());
if (helper.match() && apiAuthenticate(request)) {
switch (request->method()) {
case HTTP_HEAD:
return true;
case HTTP_PUT:
if (!_apiIsJsonContent(request)) {
if (!is_json(request)) {
return false;
}
if (!_put) {
return false;
}
// fallthrough!
case HTTP_GET:
if (!_get) {
return false;
@ -440,15 +469,15 @@ public:
default:
return false;
}
_apiAttachHelper(*request, std::move(helper));
attach_helper(*request, std::move(helper));
return true;
}
return false;
}
void _handleGet(AsyncWebServerRequest* request, ApiRequest& apireq) {
DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE);
void _handleGet(AsyncWebServerRequest* request, Request& apireq) {
DynamicJsonBuffer jsonBuffer(BufferSize);
JsonObject& root = jsonBuffer.createObject();
if (!_get(apireq, root)) {
request->send(500);
@ -456,7 +485,8 @@ public:
}
if (!apireq.done()) {
AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1);
auto* response = request->beginResponseStream(
content_type::Json.toString(), root.measureLength() + 1);
root.printTo(*response);
request->send(response);
return;
@ -468,16 +498,17 @@ public:
void _handlePut(AsyncWebServerRequest* request, uint8_t* data, size_t size) {
// XXX: arduinojson v5 de-serializer will happily read garbage from raw ptr, since there's no length limit
// this is fixed in v6 though. for now, use a wrapper, but be aware that this actually uses more mem for the jsonbuffer
DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE);
ReadOnlyStream stream(data, size);
auto* ptr = reinterpret_cast<const char*>(data);
auto reader = StringView(ptr, ptr + size);
JsonObject& root = jsonBuffer.parseObject(stream);
DynamicJsonBuffer jsonBuffer(BufferSize);
JsonObject& root = jsonBuffer.parseObject(reader);
if (!root.success()) {
request->send(500);
return;
}
auto& helper = *reinterpret_cast<ApiRequestHelper*>(request->_tempObject);
auto& helper = *reinterpret_cast<RequestHelper*>(request->_tempObject);
auto apireq = helper.request();
if (!_put(apireq, root)) {
@ -499,7 +530,14 @@ public:
}
void handleRequest(AsyncWebServerRequest* request) override {
auto& helper = *reinterpret_cast<ApiRequestHelper*>(request->_tempObject);
if (!accepts_json(request)) {
request->send(406,
content_type::Text.toString(),
content_type::Json.toString());
return;
}
auto& helper = *reinterpret_cast<RequestHelper*>(request->_tempObject);
switch (request->method()) {
case HTTP_HEAD:
@ -522,17 +560,12 @@ public:
}
}
const String& pattern() const {
return ApiBaseWebHandler::pattern();
}
const PathParts& parts() const {
return ApiBaseWebHandler::parts();
}
using BaseWebHandler::pattern;
using BaseWebHandler::parts;
private:
ApiJsonHandler _get;
ApiJsonHandler _put;
JsonHandler _get;
JsonHandler _put;
};
// ESPurna legacy API configuration
@ -541,13 +574,13 @@ private:
// MUST correctly override isRequestHandlerTrivial() to allow auth with PUT
// (i.e. so that ESPAsyncWebServer parses the body and adds form-data to request params list)
class ApiBasicWebHandler final : public ApiBaseWebHandler {
class BasicWebHandler final : public BaseWebHandler {
public:
template <typename Path, typename Callback>
ApiBasicWebHandler(Path&& path, Callback&& get, Callback&& put) :
ApiBaseWebHandler(std::forward<Path>(path)),
_get(std::forward<Callback>(get)),
_put(std::forward<Callback>(put))
template <typename Path, typename Get, typename Put>
BasicWebHandler(Path&& path, Get&& get, Put&& put) :
BaseWebHandler(std::forward<Path>(path)),
_get(std::forward<Get>(get)),
_put(std::forward<Put>(put))
{}
bool isRequestHandlerTrivial() override {
@ -559,16 +592,12 @@ public:
return false;
}
if (!_apiAcceptsText(request)) {
return false;
}
switch (request->method()) {
case HTTP_HEAD:
case HTTP_GET:
break;
case HTTP_PUT:
if (!_apiIsFormDataContent(request)) {
if (!is_form_data(request)) {
return false;
}
break;
@ -576,9 +605,9 @@ public:
return false;
}
auto helper = ApiRequestHelper(*request, parts());
auto helper = RequestHelper(*request, parts());
if (helper.match()) {
_apiAttachHelper(*request, std::move(helper));
attach_helper(*request, std::move(helper));
return true;
}
@ -591,6 +620,13 @@ public:
return;
}
if (!accepts_text(request)) {
request->send(406,
content_type::Text.toString(),
content_type::Text.toString());
return;
}
auto method = request->method();
const bool is_put = (
(!apiRestFul()|| (HTTP_PUT == method))
@ -604,7 +640,7 @@ public:
case HTTP_GET:
case HTTP_PUT: {
auto& helper = *reinterpret_cast<ApiRequestHelper*>(request->_tempObject);
auto& helper = *reinterpret_cast<RequestHelper*>(request->_tempObject);
auto apireq = helper.request();
if (is_put) {
@ -637,57 +673,76 @@ public:
}
}
const ApiBasicHandler& get() const {
const BasicHandler& get() const {
return _get;
}
const ApiBasicHandler& put() const {
const BasicHandler& put() const {
return _put;
}
const String& pattern() const {
return ApiBaseWebHandler::pattern();
}
const PathParts& parts() const {
return ApiBaseWebHandler::parts();
}
using BaseWebHandler::pattern;
using BaseWebHandler::parts;
private:
ApiBasicHandler _get;
ApiBasicHandler _put;
BasicHandler _get;
BasicHandler _put;
};
// -----------------------------------------------------------------------------
namespace internal {
namespace {
std::forward_list<BaseWebHandler*> list;
} // namespace internal
std::forward_list<ApiBaseWebHandler*> _apis;
namespace simple {
template <typename Handler, typename Callback>
void _apiRegister(const String& path, Callback&& get, Callback&& put) {
// `String` is a given, since we *do* need to construct this dynamically in sensors
auto* ptr = new Handler(String(F(API_BASE_PATH)) + path, std::forward<Callback>(get), std::forward<Callback>(put));
webServer().addHandler(reinterpret_cast<AsyncWebHandler*>(ptr));
_apis.emplace_front(ptr);
bool ok(Request& request) {
STRING_VIEW_INLINE(Ok, "OK");
request.send(Ok.toString());
return true;
}
} // namespace
bool error(ApiRequest& request) {
STRING_VIEW_INLINE(Error, "ERROR");
request.send(Error.toString());
return true;
}
} // namespace simple
STRING_VIEW_INLINE(BasePath, API_BASE_PATH);
void apiRegister(const String& path, ApiBasicHandler&& get, ApiBasicHandler&& put) {
_apiRegister<ApiBasicWebHandler>(path, std::move(get), std::move(put));
void add(BaseWebHandler* ptr) {
webServer().addHandler(ptr);
internal::list.emplace_front(ptr);
}
void apiRegister(const String& path, ApiJsonHandler&& get, ApiJsonHandler&& put) {
_apiRegister<ApiJsonWebHandler>(path, std::move(get), std::move(put));
template <typename Handler, typename Get, typename Put>
void add(String path, Get&& get, Put&& put) {
add(new Handler(
BasePath + path,
std::forward<Get>(get),
std::forward<Put>(put)));
}
void apiSetup() {
apiRegister(F("list"),
[](ApiRequest& request) {
template <typename Handler, typename Get, typename Put>
void add(StringView path, Get&& get, Put&& put) {
add<Handler, Get, Put>(
path.toString(),
std::forward<Get>(get),
std::forward<Put>(put));
}
void setup() {
add<BasicWebHandler, BasicHandler>(
STRING_VIEW("list"),
[](Request& request) {
String paths;
for (auto& api : _apis) {
paths += api->pattern() + "\r\n";
for (auto& api : internal::list) {
paths += api->pattern();
paths += '\r';
paths += '\n';
}
request.send(paths);
return true;
@ -695,27 +750,45 @@ void apiSetup() {
nullptr
);
apiRegister(F("rpc"),
add<BasicWebHandler, BasicHandler>(
STRING_VIEW("rpc"),
nullptr,
[](ApiRequest& request) {
if (rpcHandleAction(request.param(F("action")))) {
return apiOk(request);
[](Request& request) {
STRING_VIEW_INLINE(Action, "action");
if (rpcHandleAction(request.param(Action.toString()))) {
return simple::ok(request);
}
return apiError(request);
return simple::error(request);
}
);
}
} // namespace
} // namespace api
} // namespace espurna
// -----------------------------------------------------------------------------
void apiRegister(String path, espurna::api::BasicHandler&& get, espurna::api::BasicHandler&& put) {
using namespace espurna::api;
add<BasicWebHandler>(std::move(path), std::move(get), std::move(put));
}
void apiRegister(String path, espurna::api::JsonHandler&& get, espurna::api::JsonHandler&& put) {
using namespace espurna::api;
add<JsonWebHandler>(std::move(path), std::move(get), std::move(put));
}
void apiSetup() {
espurna::api::setup();
}
bool apiOk(ApiRequest& request) {
request.send(F("OK"));
return true;
return espurna::api::simple::ok(request);
}
bool apiError(ApiRequest& request) {
request.send(F("ERROR"));
return true;
return espurna::api::simple::error(request);
}
#endif // API_SUPPORT

+ 28
- 21
code/espurna/api.h View File

@ -9,33 +9,40 @@ Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#pragma once
#include "espurna.h"
#include <functional>
#include <ArduinoJson.h>
#include "api_path.h"
#include "api_impl.h"
#include "web.h"
#include <functional>
namespace espurna {
namespace api {
#if WEB_SUPPORT
bool apiAuthenticateHeader(AsyncWebServerRequest*, const String& key);
bool apiAuthenticateParam(AsyncWebServerRequest*, const String& key);
bool apiAuthenticate(AsyncWebServerRequest*);
#endif
using BasicHandler = std::function<bool(Request&)>;
using JsonHandler = std::function<bool(Request&, JsonObject& reponse)>;
void apiCommonSetup();
bool apiEnabled();
bool apiRestFul();
String apiKey();
} // namespace api
} // namespace espurna
#if WEB_SUPPORT
using ApiBasicHandler = std::function<bool(ApiRequest&)>;
using ApiJsonHandler = std::function<bool(ApiRequest&, JsonObject& reponse)>;
using ApiRequest = espurna::api::Request;
using ApiBasicHandler = espurna::api::BasicHandler;
using ApiJsonHandler = espurna::api::JsonHandler;
void apiRegister(const String& path, ApiBasicHandler&& get, ApiBasicHandler&& put);
void apiRegister(const String& path, ApiJsonHandler&& get, ApiJsonHandler&& put);
#endif
void apiRegister(String path,
espurna::api::BasicHandler&& get,
espurna::api::BasicHandler&& put);
void apiSetup();
void apiRegister(String path,
espurna::api::JsonHandler&& get,
espurna::api::JsonHandler&& put);
bool apiError(ApiRequest&);
bool apiOk(ApiRequest&);
bool apiError(espurna::api::Request&);
bool apiOk(espurna::api::Request&);
String apiKey();
bool apiEnabled();
bool apiRestFul();
void apiCommonSetup();
void apiSetup();

+ 17
- 0
code/espurna/api_async_server.h View File

@ -0,0 +1,17 @@
/*
API MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#pragma once
#include <ESPAsyncWebServer.h>
bool apiAuthenticateHeader(AsyncWebServerRequest*, const String& key);
bool apiAuthenticateParam(AsyncWebServerRequest*, const String& key);
bool apiAuthenticate(AsyncWebServerRequest*);

+ 102
- 33
code/espurna/api_common.cpp View File

@ -9,49 +9,87 @@ Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include "espurna.h"
#if WEB_SUPPORT
#include "api.h"
#include "ws.h"
#include "web.h"
#include "ws.h"
#endif
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
namespace espurna {
namespace api {
namespace {
namespace build {
bool _apiWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "api", 3) == 0);
constexpr bool enabled() {
return 1 == API_ENABLED;
}
void _apiWebSocketOnConnected(JsonObject& root) {
root["apiEnabled"] = apiEnabled();
root["apiKey"] = apiKey();
root["apiRestFul"] = apiRestFul();
root["apiRealTime"] = getSetting("apiRealTime", 1 == API_REAL_TIME_VALUES);
constexpr bool restful() {
return 1 == API_RESTFUL;
}
STRING_VIEW_INLINE(Key, API_KEY);
constexpr StringView key() {
return Key;
}
// -----------------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------------
} // namespace build
bool apiEnabled() {
return getSetting("apiEnabled", 1 == API_ENABLED);
namespace settings {
namespace keys {
STRING_VIEW_INLINE(Enabled, "apiEnabled");
STRING_VIEW_INLINE(Restful, "apiRestFul");
STRING_VIEW_INLINE(Key, "apiKey");
} // namespace keys
bool enabled() {
return getSetting(keys::Enabled, build::enabled());
}
bool apiRestFul() {
return getSetting("apiRestFul", 1 == API_RESTFUL);
bool restful() {
return getSetting(keys::Restful, build::restful());
}
String apiKey() {
return getSetting("apiKey", API_KEY);
String key() {
return getSetting(keys::Key, build::key());
}
bool apiAuthenticateHeader(AsyncWebServerRequest* request, const String& key) {
if (apiEnabled() && key.length()) {
auto* header = request->getHeader(F("Api-Key"));
} // namespace settings
namespace web {
#if WEB_SUPPORT
bool onKeyCheck(espurna::StringView key, const JsonVariant&) {
return espurna::settings::query::samePrefix(key, STRING_VIEW("api"));
}
void onVisible(JsonObject& root) {
wsPayloadModule(root, PSTR("api"));
}
void onConnected(JsonObject& root) {
root[settings::keys::Enabled] = apiEnabled();
root[settings::keys::Key] = apiKey();
root[settings::keys::Restful] = apiRestFul();
}
void setup() {
wsRegister()
.onVisible(onVisible)
.onConnected(onConnected)
.onKeyCheck(onKeyCheck);
}
bool authenticate_header(AsyncWebServerRequest* request, const String& key) {
STRING_VIEW_INLINE(Header, "Api-Key");
if (settings::enabled() && key.length()) {
auto* header = request->getHeader(Header.toString());
if (header && (key == header->value())) {
return true;
}
@ -60,8 +98,10 @@ bool apiAuthenticateHeader(AsyncWebServerRequest* request, const String& key) {
return false;
}
bool apiAuthenticateParam(AsyncWebServerRequest* request, const String& key) {
auto* param = request->getParam("apikey", (request->method() == HTTP_PUT));
bool authenticate_param(AsyncWebServerRequest* request, const String& key) {
STRING_VIEW_INLINE(Param, "apikey");
auto* param = request->getParam(Param.toString(), (request->method() == HTTP_PUT));
if (param && (key == param->value())) {
return true;
}
@ -69,28 +109,57 @@ bool apiAuthenticateParam(AsyncWebServerRequest* request, const String& key) {
return false;
}
bool apiAuthenticate(AsyncWebServerRequest* request) {
bool authenticate(AsyncWebServerRequest* request) {
const auto key = apiKey();
if (!key.length()) {
return false;
}
if (apiAuthenticateHeader(request, key)) {
if (authenticate_header(request, key)) {
return true;
}
if (apiAuthenticateParam(request, key)) {
if (authenticate_param(request, key)) {
return true;
}
return false;
}
void apiCommonSetup() {
wsRegister()
.onVisible([](JsonObject& root) { root["apiVisible"] = 1; })
.onConnected(_apiWebSocketOnConnected)
.onKeyCheck(_apiWebSocketOnKeyCheck);
#endif
} // namespace web
} // namespace
} // namespace api
} // namespace espurna
#if WEB_SUPPORT
bool apiAuthenticateHeader(AsyncWebServerRequest* request, const String& key) {
return espurna::api::web::authenticate_header(request, key);
}
bool apiAuthenticateParam(AsyncWebServerRequest* request, const String& key) {
return espurna::api::web::authenticate_param(request, key);
}
bool apiAuthenticate(AsyncWebServerRequest* request) {
return espurna::api::web::authenticate(request);
}
#endif
String apiKey() {
return espurna::api::settings::key();
}
bool apiEnabled() {
return espurna::api::settings::enabled();
}
#endif // WEB_SUPPORT == 1
bool apiRestFul() {
return espurna::api::settings::restful();
}
void apiCommonSetup() {
#if WEB_SUPPORT
espurna::api::web::setup();
#endif
}

+ 36
- 40
code/espurna/api_impl.h View File

@ -10,23 +10,28 @@ Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <algorithm>
#include <memory>
#include <vector>
#include "api_async_server.h"
#include "api_path.h"
// this is a purely temporary object, which we can only create while doing the API dispatch
namespace espurna {
namespace api {
// temporary object, which we can only create while doing the API dispatch
struct Request {
Request() = delete;
struct ApiRequest {
ApiRequest() = delete;
Request(const Request&) = default;
Request& operator=(const Request&) = delete;
ApiRequest(const ApiRequest&) = default;
ApiRequest(ApiRequest&&) noexcept = default;
Request(Request&&) noexcept = default;
Request& operator=(Request&&) = delete;
explicit ApiRequest(AsyncWebServerRequest& request, const PathParts& pattern, const PathParts& parts) :
Request(AsyncWebServerRequest& request, const PathParts& pattern, const PathParts& parts) :
_request(request),
_pattern(pattern),
_parts(parts)
@ -57,26 +62,6 @@ struct ApiRequest {
});
}
const String& param(const String& name) {
auto* result = _request.getParam(name, HTTP_PUT == _request.method());
if (result) {
return result->value();
}
return _empty_string();
}
void send(const String& payload) {
if (_done) return;
_done = true;
if (payload.length()) {
_request.send(200, "text/plain", payload);
} else {
_request.send(204);
}
}
bool done() const {
return _done;
}
@ -86,7 +71,7 @@ struct ApiRequest {
}
String part(size_t index) const {
return _parts[index];
return _parts[index].toString();
}
// Only works when pattern cointains '+', retrieving the part at the same index from the real path
@ -94,12 +79,15 @@ struct ApiRequest {
String wildcard(int index) const;
size_t wildcards() const;
private:
const String& _empty_string() const {
static const String string;
return string;
}
// Extract form data parameter value from request by name
StringView param(const String&);
// Send out the payload and finish the request
// By default, payload is sent with status 200
// For zero-length payloads, status is set to 204
void send(const String& payload);
private:
bool _done { false };
AsyncWebServerRequest& _request;
@ -107,20 +95,25 @@ private:
const PathParts& _parts;
};
struct ApiRequestHelper {
ApiRequestHelper(const ApiRequestHelper&) = delete;
ApiRequestHelper(ApiRequestHelper&&) noexcept = default;
struct RequestHelper {
RequestHelper() = delete;
RequestHelper(const RequestHelper&) = delete;
RequestHelper& operator=(const RequestHelper&) = delete;
RequestHelper(RequestHelper&&) noexcept = default;
RequestHelper& operator=(RequestHelper&&) = delete;
// &path is expected to be request->url(), which is valid throughout the request's lifetime
explicit ApiRequestHelper(AsyncWebServerRequest& request, const PathParts& pattern) :
RequestHelper(AsyncWebServerRequest& request, const PathParts& pattern) :
_request(request),
_pattern(pattern),
_path(request.url()),
_match(_pattern.match(_path))
{}
ApiRequest request() const {
return ApiRequest(_request, _pattern, _path);
Request request() const {
return Request(_request, _pattern, _path);
}
const PathParts& parts() const {
@ -137,3 +130,6 @@ private:
PathParts _path;
bool _match;
};
} // namespace api
} // namespace espurna

+ 50
- 13
code/espurna/api_path.h View File

@ -9,9 +9,10 @@ Copyright (C) 2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#pragma once
#include <Arduino.h>
#include <vector>
#include "types.h"
// -----------------------------------------------------------------------------
struct PathPart {
@ -31,11 +32,24 @@ struct PathParts {
using Parts = std::vector<PathPart>;
PathParts() = delete;
PathParts(const PathParts&) = delete;
PathParts(const PathParts&) = default;
PathParts(PathParts&&) noexcept = default;
explicit PathParts(espurna::StringView path);
PathParts(espurna::StringView path, Parts&& parts) :
_path(path),
_parts(std::move(parts)),
_ok(_parts.size())
{}
explicit PathParts(const String& path);
PathParts(PathParts&& other) noexcept :
PathParts(other._path, std::move(other._parts))
{}
PathParts(espurna::StringView path, PathParts&& other) noexcept :
_path(path),
_parts(std::move(other._parts)),
_ok(other._ok)
{}
explicit operator bool() const {
return _ok;
@ -49,12 +63,19 @@ struct PathParts {
_parts.reserve(size);
}
String operator[](size_t index) const {
auto& part = _parts[index];
return _path.substring(part.offset, part.offset + part.length);
espurna::StringView operator[](size_t index) const {
return get(_parts[index]);
}
espurna::StringView back() const {
return get(_parts.back());
}
espurna::StringView front() const {
return get(_parts.front());
}
const String& path() const {
espurna::StringView path() const {
return _path;
}
@ -75,18 +96,34 @@ struct PathParts {
}
bool match(const PathParts& path) const;
bool match(const String& path) const {
bool match(espurna::StringView path) const {
return match(PathParts(path));
}
static espurna::StringView wildcard(const PathParts& pattern, const PathParts& value, int index);
static size_t wildcards(const PathParts& pattern);
private:
PathPart& emplace_back(PathPart::Type type, size_t offset, size_t length) {
PathPart part { type, offset, length };
_parts.push_back(std::move(part));
espurna::StringView get(const PathPart& part) const {
return espurna::StringView(
_path.begin() + part.offset,
_path.begin() + part.offset + part.length);
}
PathPart& emplace_back(PathPart part) {
_parts.push_back(part);
return _parts.back();
}
const String& _path;
PathPart& emplace_back(PathPart::Type type, size_t offset, size_t length) {
return emplace_back(PathPart{
.type = type,
.offset = offset,
.length = length
});
}
espurna::StringView _path;
Parts _parts;
bool _ok { false };
};

+ 0
- 405
code/espurna/board.cpp View File

@ -1,405 +0,0 @@
/*
BOARD MODULE
*/
#include "espurna.h"
#include "relay.h"
#include "sensor.h"
#include "utils.h"
//--------------------------------------------------------------------------------
const String& getChipId() {
static String value;
if (!value.length()) {
char buffer[7];
value.reserve(sizeof(buffer));
snprintf_P(buffer, sizeof(buffer), PSTR("%06X"), ESP.getChipId());
value = buffer;
}
return value;
}
const String& getIdentifier() {
static String value;
if (!value.length()) {
value += getAppName();
value += '-';
value += getChipId();
}
return value;
}
// Full Chip ID (aka MAC)
// based on the [esptool.py](https://github.com/espressif/esptool) implementation
// - register addresses: https://github.com/espressif/esptool/blob/737825ba8d7aa696e4a9213cad932bceafb79f51/esptool.py#L1140-L1143
// - chip id & mac: https://github.com/espressif/esptool/blob/737825ba8d7aa696e4a9213cad932bceafb79f51/esptool.py#L1235-L1254
const String& getFullChipId() {
static String out;
if (!out.length()) {
uint32_t regs[3] {
READ_PERI_REG(0x3ff00050),
READ_PERI_REG(0x3ff00054),
READ_PERI_REG(0x3ff0005c)};
uint8_t mac[6] {
0xff,
0xff,
0xff,
static_cast<uint8_t>((regs[1] >> 8ul) & 0xfful),
static_cast<uint8_t>(regs[1] & 0xffu),
static_cast<uint8_t>((regs[0] >> 24ul) & 0xffu)};
if (mac[2] != 0) {
mac[0] = (regs[2] >> 16ul) & 0xffu;
mac[1] = (regs[2] >> 8ul) & 0xffu;
mac[2] = (regs[2] & 0xffu);
} else if (0 == ((regs[1] >> 16ul) & 0xff)) {
mac[0] = 0x18;
mac[1] = 0xfe;
mac[2] = 0x34;
} else if (1 == ((regs[1] >> 16ul) & 0xff)) {
mac[0] = 0xac;
mac[1] = 0xd0;
mac[2] = 0x74;
}
char buffer[(sizeof(mac) * 2) + 1];
if (hexEncode(mac, sizeof(mac), buffer, sizeof(buffer))) {
out = buffer;
}
}
return out;
}
const char* getEspurnaModules() {
static const char modules[] PROGMEM =
#if ALEXA_SUPPORT
"ALEXA "
#endif
#if API_SUPPORT
"API "
#endif
#if BUTTON_SUPPORT
"BUTTON "
#endif
#if DEBUG_SERIAL_SUPPORT
"DEBUG_SERIAL "
#endif
#if DEBUG_TELNET_SUPPORT
"DEBUG_TELNET "
#endif
#if DEBUG_UDP_SUPPORT
"DEBUG_UDP "
#endif
#if DEBUG_WEB_SUPPORT
"DEBUG_WEB "
#endif
#if DOMOTICZ_SUPPORT
"DOMOTICZ "
#endif
#if ENCODER_SUPPORT
"ENCODER "
#endif
#if FAN_SUPPORT
"FAN "
#endif
#if HOMEASSISTANT_SUPPORT
"HOMEASSISTANT "
#endif
#if I2C_SUPPORT
"I2C "
#endif
#if INFLUXDB_SUPPORT
"INFLUXDB "
#endif
#if IR_SUPPORT
"IR "
#endif
#if LED_SUPPORT
"LED "
#endif
#if LLMNR_SUPPORT
"LLMNR "
#endif
#if MDNS_SERVER_SUPPORT
"MDNS "
#endif
#if MQTT_SUPPORT
"MQTT "
#endif
#if NETBIOS_SUPPORT
"NETBIOS "
#endif
#if NOFUSS_SUPPORT
"NOFUSS "
#endif
#if NTP_SUPPORT
"NTP "
#endif
#if OTA_ARDUINOOTA_SUPPORT
"ARDUINO_OTA "
#endif
#if (OTA_CLIENT != OTA_CLIENT_NONE)
"OTA_CLIENT "
#endif
#if PROMETHEUS_SUPPORT
"METRICS "
#endif
#if RELAY_SUPPORT
"RELAY "
#endif
#if RFM69_SUPPORT
"RFM69 "
#endif
#if RFB_SUPPORT
"RFB "
#endif
#if RPN_RULES_SUPPORT
"RPN_RULES "
#endif
#if SCHEDULER_SUPPORT
"SCHEDULER "
#endif
#if SENSOR_SUPPORT
"SENSOR "
#endif
#if SPIFFS_SUPPORT
"SPIFFS "
#endif
#if SSDP_SUPPORT
"SSDP "
#endif
#if TELNET_SUPPORT
#if TELNET_SERVER == TELNET_SERVER_WIFISERVER
"TELNET_SYNC "
#else
"TELNET "
#endif // TELNET_SERVER == TELNET_SERVER_WIFISERVER
#endif
#if TERMINAL_SUPPORT
"TERMINAL "
#endif
#if GARLAND_SUPPORT
"GARLAND "
#endif
#if THERMOSTAT_SUPPORT
"THERMOSTAT "
#endif
#if THERMOSTAT_DISPLAY_SUPPORT
"THERMOSTAT_DISPLAY "
#endif
#if THINGSPEAK_SUPPORT
"THINGSPEAK "
#endif
#if UART_MQTT_SUPPORT
"UART_MQTT "
#endif
#if WEB_SUPPORT
"WEB "
#endif
"";
return modules;
}
const char* getEspurnaWebUI() {
static const char webui[] PROGMEM =
#if WEBUI_IMAGE == WEBUI_IMAGE_SMALL
"SMALL"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_LIGHT
"LIGHT"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_SENSOR
"SENSOR"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE
"RFBRIDGE"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_RFM69
"RFM69"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX
"LIGHTFOX"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_GARLAND
"GARLAND"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT
"THERMOSTAT"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN
"CURTAIN"
#endif
#if WEBUI_IMAGE == WEBUI_IMAGE_FULL
"FULL"
#endif
"";
return webui;
}
#if SENSOR_SUPPORT
const char* getEspurnaSensors() {
static const char sensors[] PROGMEM =
#if AM2320_SUPPORT
"AM2320_I2C "
#endif
#if ANALOG_SUPPORT
"ANALOG "
#endif
#if BH1750_SUPPORT
"BH1750 "
#endif
#if BMP180_SUPPORT
"BMP180 "
#endif
#if BMX280_SUPPORT
"BMX280 "
#endif
#if BME680_SUPPORT
"BME680 "
#endif
#if CSE7766_SUPPORT
"CSE7766 "
#endif
#if DALLAS_SUPPORT
"DALLAS "
#endif
#if DHT_SUPPORT
"DHTXX "
#endif
#if DIGITAL_SUPPORT
"DIGITAL "
#endif
#if ECH1560_SUPPORT
"ECH1560 "
#endif
#if EMON_ADC121_SUPPORT
"EMON_ADC121 "
#endif
#if EMON_ADS1X15_SUPPORT
"EMON_ADX1X15 "
#endif
#if EMON_ANALOG_SUPPORT
"EMON_ANALOG "
#endif
#if EVENTS_SUPPORT
"EVENTS "
#endif
#if GEIGER_SUPPORT
"GEIGER "
#endif
#if GUVAS12SD_SUPPORT
"GUVAS12SD "
#endif
#if HLW8012_SUPPORT
"HLW8012 "
#endif
#if LDR_SUPPORT
"LDR "
#endif
#if MHZ19_SUPPORT
"MHZ19 "
#endif
#if MICS2710_SUPPORT
"MICS2710 "
#endif
#if MICS5525_SUPPORT
"MICS5525 "
#endif
#if NTC_SUPPORT
"NTC "
#endif
#if PMSX003_SUPPORT
"PMSX003 "
#endif
#if PULSEMETER_SUPPORT
"PULSEMETER "
#endif
#if PZEM004T_SUPPORT
"PZEM004T "
#endif
#if SDS011_SUPPORT
"SDS011 "
#endif
#if SENSEAIR_SUPPORT
"SENSEAIR "
#endif
#if SHT3X_I2C_SUPPORT
"SHT3X_I2C "
#endif
#if SI7021_SUPPORT
"SI7021 "
#endif
#if SM300D2_SUPPORT
"SM300D2 "
#endif
#if SONAR_SUPPORT
"SONAR "
#endif
#if T6613_SUPPORT
"T6613 "
#endif
#if TMP3X_SUPPORT
"TMP3X "
#endif
#if V9261F_SUPPORT
"V9261F "
#endif
#if VEML6075_SUPPORT
"VEML6075 "
#endif
#if VL53L1X_SUPPORT
"VL53L1X "
#endif
#if EZOPH_SUPPORT
"EZOPH "
#endif
#if ADE7953_SUPPORT
"ADE7953 "
#endif
#if SI1145_SUPPORT
"SI1145 "
#endif
"";
return sensors;
}
#endif
bool haveRelaysOrSensors() {
bool result = false;
result = (relayCount() > 0);
#if SENSOR_SUPPORT
result = result || (magnitudeCount() > 0);
#endif
return result;
}
void boardSetup() {
#if DEBUG_SERIAL_SUPPORT
if (debugLogBuffer()) {
return;
}
DEBUG_MSG_P(PSTR("[MAIN] %s %s built %s\n"), getAppName(), getVersion(), buildTime().c_str());
DEBUG_MSG_P(PSTR("[MAIN] %s\n"), getAppAuthor());
DEBUG_MSG_P(PSTR("[MAIN] %s\n"), getAppWebsite());
DEBUG_MSG_P(PSTR("[MAIN] CPU chip ID: %s\n"), getFullChipId().c_str());
DEBUG_MSG_P(PSTR("[MAIN] SDK: %s\n"), ESP.getSdkVersion());
DEBUG_MSG_P(PSTR("[MAIN] Arduino Core: %s\n"), getCoreVersion().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Support: %s\n"), getEspurnaModules());
#if SENSOR_SUPPORT
DEBUG_MSG_P(PSTR("[MAIN] Sensors: %s\n"), getEspurnaSensors());
#endif
#endif
}

+ 0
- 19
code/espurna/board.h View File

@ -1,19 +0,0 @@
/*
BOARD MODULE
*/
#pragma once
#include <Arduino.h>
const String& getChipId();
const String& getFullChipId();
const String& getIdentifier();
const char* getEspurnaModules();
const char* getEspurnaSensors();
const char* getEspurnaWebUI();
void boardSetup();

+ 478
- 0
code/espurna/build.cpp View File

@ -0,0 +1,478 @@
/*
BUILD INFO
*/
#include "espurna.h"
#include "utils.h"
#include <cstring>
//--------------------------------------------------------------------------------
namespace espurna {
namespace build {
namespace {
namespace sdk {
espurna::StringView base() {
// aka `const char SDK_VERSION[]`
return system_get_sdk_version();
}
espurna::StringView core_version() {
static const String out = ([]() {
String out;
#ifdef ARDUINO_ESP8266_RELEASE
out = ESP.getCoreVersion();
if (out.equals("00000000")) {
out = String(ARDUINO_ESP8266_RELEASE);
}
out.replace('_', '.');
#else
#define _GET_COREVERSION_STR(X) #X
#define GET_COREVERSION_STR(X) _GET_COREVERSION_STR(X)
out = GET_COREVERSION_STR(ARDUINO_ESP8266_GIT_DESC);
#undef _GET_COREVERSION_STR
#undef GET_COREVERSION_STR
#endif
return out;
})();
return out;
}
espurna::StringView core_revision() {
static const String out = ([]() {
#ifdef ARDUINO_ESP8266_GIT_VER
return String(ARDUINO_ESP8266_GIT_VER, 16);
#else
return PSTR("(unspecified)");
#endif
})();
return out;
}
Sdk get() {
return Sdk{
.base = base(),
.version = core_version(),
.revision = core_revision(),
};
}
} // namespace sdk
namespace hardware {
namespace internal {
PROGMEM_STRING(Manufacturer, MANUFACTURER);
PROGMEM_STRING(Device, DEVICE);
} // namespace internal
constexpr StringView manufacturer() {
return internal::Manufacturer;
}
constexpr StringView device() {
return internal::Device;
}
constexpr Hardware get() {
return Hardware{
.manufacturer = manufacturer(),
.device = device(),
};
}
} // namespace device
namespace app {
namespace internal {
alignas(4) static constexpr char Modules[] PROGMEM_STRING_ATTR =
#if ALEXA_SUPPORT
"ALEXA "
#endif
#if API_SUPPORT
"API "
#endif
#if BUTTON_SUPPORT
"BUTTON "
#endif
#if DEBUG_SERIAL_SUPPORT
"DEBUG_SERIAL "
#endif
#if DEBUG_TELNET_SUPPORT
"DEBUG_TELNET "
#endif
#if DEBUG_UDP_SUPPORT
"DEBUG_UDP "
#endif
#if DEBUG_WEB_SUPPORT
"DEBUG_WEB "
#endif
#if DOMOTICZ_SUPPORT
"DOMOTICZ "
#endif
#if ENCODER_SUPPORT
"ENCODER "
#endif
#if FAN_SUPPORT
"FAN "
#endif
#if HOMEASSISTANT_SUPPORT
"HOMEASSISTANT "
#endif
#if I2C_SUPPORT
"I2C "
#endif
#if INFLUXDB_SUPPORT
"INFLUXDB "
#endif
#if IR_SUPPORT
"IR "
#endif
#if LED_SUPPORT
"LED "
#endif
#if LLMNR_SUPPORT
"LLMNR "
#endif
#if MDNS_SERVER_SUPPORT
"MDNS "
#endif
#if MQTT_SUPPORT
"MQTT "
#endif
#if NETBIOS_SUPPORT
"NETBIOS "
#endif
#if NOFUSS_SUPPORT
"NOFUSS "
#endif
#if NTP_SUPPORT
"NTP "
#endif
#if OTA_ARDUINOOTA_SUPPORT
"ARDUINO_OTA "
#endif
#if OTA_WEB_SUPPORT
"OTA_WEB "
#endif
#if (OTA_CLIENT != OTA_CLIENT_NONE)
"OTA_CLIENT "
#endif
#if PROMETHEUS_SUPPORT
"METRICS "
#endif
#if RELAY_SUPPORT
"RELAY "
#endif
#if RFM69_SUPPORT
"RFM69 "
#endif
#if RFB_SUPPORT
"RFB "
#endif
#if RPN_RULES_SUPPORT
"RPN_RULES "
#endif
#if SCHEDULER_SUPPORT
"SCHEDULER "
#endif
#if SENSOR_SUPPORT
"SENSOR "
#endif
#if SPIFFS_SUPPORT
"SPIFFS "
#endif
#if SSDP_SUPPORT
"SSDP "
#endif
#if TELNET_SUPPORT
"TELNET "
#endif
#if TERMINAL_SUPPORT
"TERMINAL "
#endif
#if GARLAND_SUPPORT
"GARLAND "
#endif
#if THERMOSTAT_SUPPORT
"THERMOSTAT "
#endif
#if THERMOSTAT_DISPLAY_SUPPORT
"THERMOSTAT_DISPLAY "
#endif
#if THINGSPEAK_SUPPORT
"THINGSPEAK "
#endif
#if UART_SUPPORT
#if UART_SUPPORT_SOFTWARE
"UART+SW "
#else
"UART "
#endif
#endif
#if UART_MQTT_SUPPORT
"UART_MQTT "
#endif
#if WEB_SUPPORT
#if WEBUI_IMAGE == WEBUI_IMAGE_SMALL
"WEB_SMALL "
#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHT
"WEB_LIGHT "
#elif WEBUI_IMAGE == WEBUI_IMAGE_SENSOR
"WEB_SENSOR "
#elif WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE
"WEB_RFBRIDGE "
#elif WEBUI_IMAGE == WEBUI_IMAGE_RFM69
"WEB_RFM69 "
#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX
"WEB_LIGHTFOX "
#elif WEBUI_IMAGE == WEBUI_IMAGE_GARLAND
"WEB_GARLAND "
#elif WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT
"WEB_THERMOSTAT "
#elif WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN
"WEB_CURTAIN "
#elif WEBUI_IMAGE == WEBUI_IMAGE_FULL
"WEB_FULL "
#endif
#endif
"";
PROGMEM_STRING(Name, APP_NAME);
PROGMEM_STRING(Version, APP_VERSION);
PROGMEM_STRING(Author, APP_AUTHOR);
PROGMEM_STRING(Website, APP_WEBSITE);
PROGMEM_STRING(BuildDate, __DATE__);
PROGMEM_STRING(BuildTime, __TIME__);
} // namespace internal
// ref. https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html
//
// __DATE__
// > This macro expands to a string constant that describes the date on which the preprocessor is being run.
// > The string constant contains eleven characters and looks like "Feb 12 1996". If the day of the month is less than 10,
// > it is padded with a space on the left.
// >
// > If GCC cannot determine the current date, it will emit a warning message (once per compilation) and __DATE__ will expand to "??? ?? ????".
//
// __TIME__
// > This macro expands to a string constant that describes the time at which the preprocessor is being run.
// > The string constant contains eight characters and looks like "23:59:01".
// >
// > If GCC cannot determine the current time, it will emit a warning message (once per compilation) and __TIME__ will expand to "??:??:??".
namespace time {
// "Jan 1 1970"
// ^^^
constexpr StringView raw_month() {
return StringView(&internal::BuildDate[0], &internal::BuildDate[3]);
}
static_assert(raw_month().length() == 3, "");
// "Jan 1 1970"
// ^^ (with space, or without)
constexpr StringView raw_day() {
return (internal::BuildDate[4] == ' ')
? StringView(&internal::BuildDate[5], &internal::BuildDate[6])
: StringView(&internal::BuildDate[4], &internal::BuildDate[6]);
}
static_assert(raw_day().length() < 3, "");
// "Jan 1 1970"
// ^^^^
constexpr StringView raw_year() {
return StringView(&internal::BuildDate[7], &internal::BuildDate[11]);
}
static_assert(raw_year().length() == 4, "");
// "03:00:00"
// ^^
constexpr StringView raw_hour() {
return StringView(&internal::BuildTime[0], &internal::BuildTime[2]);
}
static_assert(raw_hour().length() == 2, "");
// "03:00:00"
// ^^
constexpr StringView raw_minute() {
return StringView(&internal::BuildTime[3], &internal::BuildTime[5]);
}
static_assert(raw_minute().length() == 2, "");
// "03:00:00"
// ^^
constexpr StringView raw_second() {
return StringView(&internal::BuildTime[6], &internal::BuildTime[8]);
}
static_assert(raw_second().length() == 2, "");
#define STRING_EQUALS(EXPECTED, ACTUAL)\
(__builtin_memcmp((ACTUAL).c_str(), (EXPECTED), (ACTUAL).length()) == 0)
constexpr int from_raw_month(StringView month) {
return STRING_EQUALS("Jan", month) ? 1 :
STRING_EQUALS("Feb", month) ? 2 :
STRING_EQUALS("Mar", month) ? 3 :
STRING_EQUALS("Apr", month) ? 4 :
STRING_EQUALS("May", month) ? 5 :
STRING_EQUALS("Jun", month) ? 6 :
STRING_EQUALS("Jul", month) ? 7 :
STRING_EQUALS("Aug", month) ? 8 :
STRING_EQUALS("Sep", month) ? 9 :
STRING_EQUALS("Oct", month) ? 10 :
STRING_EQUALS("Nov", month) ? 11 :
STRING_EQUALS("Dec", month) ? 12 : 0;
}
#undef STRING_EQUALS
constexpr int month() {
return from_raw_month(raw_month());
}
constexpr int from_one_digit(char value) {
return ((value >= '0') && (value <= '9'))
? (value - '0')
: 0;
}
constexpr int from_two_digits(StringView value) {
return (from_one_digit(value.c_str()[0]) * 10)
+ from_one_digit(value.c_str()[1]);
}
constexpr int from_raw_day(StringView day) {
return (day.length() == 2)
? from_two_digits(day)
: from_one_digit(*day.c_str());
}
constexpr int day() {
return from_raw_day(raw_day());
}
constexpr int hour() {
return from_two_digits(raw_hour());
}
constexpr int minute() {
return from_two_digits(raw_minute());
}
constexpr int second() {
return from_two_digits(raw_second());
}
constexpr int from_raw_year(StringView year) {
return (from_one_digit(year.c_str()[0]) * 1000)
+ (from_one_digit(year.c_str()[1]) * 100)
+ (from_one_digit(year.c_str()[2]) * 10)
+ from_one_digit(year.c_str()[3]);
}
constexpr int year() {
return from_raw_year(raw_year());
}
} // namespace time
constexpr StringView modules() {
return internal::Modules;
}
constexpr StringView name() {
return internal::Name;
}
constexpr StringView version() {
return internal::Version;
}
constexpr StringView author() {
return internal::Author;
}
constexpr StringView website() {
return internal::Website;
}
StringView build_time() {
// 1234-56-78 01:02:03
static char out[20] = {0};
if (out[0] == '\0') {
// workaround for gcc4.8, explicitly mark as constexpr
// otherwise, we will read progmem'ed string at runtime
// (double-check the asm when changing anything here)
constexpr int year = time::year();
constexpr int month = time::month();
constexpr int day = time::day();
constexpr int hour = time::hour();
constexpr int minute = time::minute();
constexpr int second = time::second();
snprintf_P(out, sizeof(out),
PSTR("%4d-%02d-%02d %02d:%02d:%02d"),
year, month, day,
hour, minute, second);
}
return StringView(out, sizeof(out) - 1);
}
App get() {
return App{
.name = name(),
.version = version(),
.build_time = build_time(),
.author = author(),
.website = website(),
};
};
} // namespace app
Info info() {
return Info{
.sdk = sdk::get(),
.hardware = hardware::get(),
.app = app::get(),
};
}
} // namespace
} // namespace build
} // namespace espurna
espurna::StringView buildTime() {
return espurna::build::app::build_time();
}
espurna::build::Sdk buildSdk() {
return espurna::build::sdk::get();
}
espurna::build::Hardware buildHardware() {
return espurna::build::hardware::get();
}
espurna::build::App buildApp() {
return espurna::build::app::get();
}
espurna::build::Info buildInfo() {
return espurna::build::info();
}
espurna::StringView buildModules() {
return espurna::build::app::modules();
}

+ 51
- 0
code/espurna/build.h View File

@ -0,0 +1,51 @@
/*
BUILD INFO
*/
#pragma once
#include <Arduino.h>
#include "types.h"
namespace espurna {
namespace build {
struct Sdk {
StringView base;
StringView version;
StringView revision;
};
struct Hardware {
StringView manufacturer;
StringView device;
};
struct App {
StringView name;
StringView version;
StringView build_time;
StringView author;
StringView website;
};
struct Info {
Sdk sdk;
Hardware hardware;
App app;
};
} // namespace build
} // namespace espruna
espurna::StringView buildTime();
espurna::build::Info buildInfo();
espurna::build::Sdk buildSdk();
espurna::build::Hardware buildHardware();
espurna::build::App buildApp();
espurna::StringView buildModules();

+ 1403
- 555
code/espurna/button.cpp
File diff suppressed because it is too large
View File


+ 8
- 54
code/espurna/button.h View File

@ -8,24 +8,17 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#pragma once
#include "espurna.h"
#include <Arduino.h>
#include "libs/BasePin.h"
#include "libs/DebounceEvent.h"
#include <cstdint>
#include <cstddef>
#include <memory>
constexpr size_t ButtonsActionMax { 255ul };
constexpr size_t ButtonsPresetMax { 8ul };
constexpr size_t ButtonsMax { 32ul };
enum class ButtonProvider : int {
None,
Gpio,
Analog
};
enum class ButtonEvent {
None,
Pressed,
@ -34,12 +27,10 @@ enum class ButtonEvent {
DoubleClick,
LongClick,
LongLongClick,
TripleClick
TripleClick,
};
// button actions, limited to 8-bit number (0b11111111 / 0xff / 255)
enum class ButtonAction : uint8_t {
enum class ButtonAction {
None,
Toggle,
On,
@ -56,53 +47,16 @@ enum class ButtonAction : uint8_t {
Custom,
FanLow,
FanMedium,
FanHigh
};
struct ButtonActions {
ButtonAction pressed;
ButtonAction released;
ButtonAction click;
ButtonAction dblclick;
ButtonAction lngclick;
ButtonAction lnglngclick;
ButtonAction trplclick;
};
struct ButtonEventDelays {
ButtonEventDelays();
ButtonEventDelays(unsigned long debounce, unsigned long repeat, unsigned long lngclick, unsigned long lnglngclick);
unsigned long debounce;
unsigned long repeat;
unsigned long lngclick;
unsigned long lnglngclick;
};
using ButtonEventEmitterPtr = std::unique_ptr<debounce_event::EventEmitter>;
struct button_t {
button_t(ButtonActions&& actions, ButtonEventDelays&& delays);
button_t(BasePinPtr&& pin, const debounce_event::types::Config& config,
ButtonActions&& actions, ButtonEventDelays&& delays);
bool state();
ButtonEvent loop();
ButtonEventEmitterPtr event_emitter;
ButtonActions actions;
ButtonEventDelays event_delays;
FanHigh,
TerminalCommand,
};
using ButtonEventHandler = void(*)(size_t id, ButtonEvent event);
void buttonSetCustomAction(ButtonEventHandler);
void buttonSetNotifyAction(ButtonEventHandler);
ButtonAction buttonAction(size_t id, const ButtonEvent event);
void buttonEvent(size_t id, ButtonEvent event);
bool buttonAdd();
void buttonOnEvent(ButtonEventHandler);
size_t buttonCount();
void buttonSetup();

+ 0
- 329
code/espurna/button_config.h View File

@ -1,329 +0,0 @@
/*
BUTTON MODULE
*/
#pragma once
#include "espurna.h"
namespace ButtonMask {
constexpr int Pushbutton { 1 << 0 };
constexpr int Switch { 1 << 1 };
constexpr int DefaultLow { 1 << 2 };
constexpr int DefaultHigh { 1 << 3 };
constexpr int DefaultBoot { 1 << 4 };
constexpr int SetPullup { 1 << 5 };
constexpr int SetPulldown { 1 << 6 };
} // namespace ButtonMask
namespace button {
namespace build {
constexpr size_t pin(size_t index) {
return (
(index == 0) ? BUTTON1_PIN :
(index == 1) ? BUTTON2_PIN :
(index == 2) ? BUTTON3_PIN :
(index == 3) ? BUTTON4_PIN :
(index == 4) ? BUTTON5_PIN :
(index == 5) ? BUTTON6_PIN :
(index == 6) ? BUTTON7_PIN :
(index == 7) ? BUTTON8_PIN : GPIO_NONE
);
}
constexpr GpioType pinType(size_t index) {
return (
(index == 0) ? BUTTON1_PIN_TYPE :
(index == 1) ? BUTTON2_PIN_TYPE :
(index == 2) ? BUTTON3_PIN_TYPE :
(index == 3) ? BUTTON4_PIN_TYPE :
(index == 4) ? BUTTON5_PIN_TYPE :
(index == 5) ? BUTTON6_PIN_TYPE :
(index == 6) ? BUTTON7_PIN_TYPE :
(index == 7) ? BUTTON8_PIN_TYPE : GPIO_TYPE_NONE
);
}
constexpr int configBitmask(size_t index) {
return (
(index == 0) ? (BUTTON1_CONFIG) :
(index == 1) ? (BUTTON2_CONFIG) :
(index == 2) ? (BUTTON3_CONFIG) :
(index == 3) ? (BUTTON4_CONFIG) :
(index == 4) ? (BUTTON5_CONFIG) :
(index == 5) ? (BUTTON6_CONFIG) :
(index == 6) ? (BUTTON7_CONFIG) :
(index == 7) ? (BUTTON8_CONFIG) : (BUTTON_PUSHBUTTON | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH)
);
}
namespace internal {
constexpr debounce_event::types::Config decode(int bitmask) {
return {
((bitmask & ButtonMask::Pushbutton)
? debounce_event::types::Mode::Pushbutton
: debounce_event::types::Mode::Switch),
((bitmask & ButtonMask::DefaultLow) ? debounce_event::types::PinValue::Low
: (bitmask & ButtonMask::DefaultHigh) ? debounce_event::types::PinValue::High
: (bitmask & ButtonMask::DefaultBoot) ? debounce_event::types::PinValue::Initial
: debounce_event::types::PinValue::Low),
((bitmask & ButtonMask::SetPullup) ? debounce_event::types::PinMode::InputPullup
: (bitmask & ButtonMask::SetPulldown) ? debounce_event::types::PinMode::InputPulldown
: debounce_event::types::PinMode::Input)
};
}
} // namespace internal
constexpr debounce_event::types::Mode mode(size_t index) {
return internal::decode(configBitmask(index)).mode;
}
constexpr debounce_event::types::PinValue defaultValue(size_t index) {
return internal::decode(configBitmask(index)).default_value;
}
constexpr debounce_event::types::PinMode pinMode(size_t index) {
return internal::decode(configBitmask(index)).pin_mode;
}
constexpr ButtonAction release(size_t index) {
return (
(index == 0) ? BUTTON1_RELEASE :
(index == 1) ? BUTTON2_RELEASE :
(index == 2) ? BUTTON3_RELEASE :
(index == 3) ? BUTTON4_RELEASE :
(index == 4) ? BUTTON5_RELEASE :
(index == 5) ? BUTTON6_RELEASE :
(index == 6) ? BUTTON7_RELEASE :
(index == 7) ? BUTTON8_RELEASE : BUTTON_ACTION_NONE
);
}
constexpr ButtonAction press(size_t index) {
return (
(index == 0) ? BUTTON1_PRESS :
(index == 1) ? BUTTON2_PRESS :
(index == 2) ? BUTTON3_PRESS :
(index == 3) ? BUTTON4_PRESS :
(index == 4) ? BUTTON5_PRESS :
(index == 5) ? BUTTON6_PRESS :
(index == 6) ? BUTTON7_PRESS :
(index == 7) ? BUTTON8_PRESS : BUTTON_ACTION_NONE
);
}
constexpr ButtonAction click(size_t index) {
return (
(index == 0) ? BUTTON1_CLICK :
(index == 1) ? BUTTON2_CLICK :
(index == 2) ? BUTTON3_CLICK :
(index == 3) ? BUTTON4_CLICK :
(index == 4) ? BUTTON5_CLICK :
(index == 5) ? BUTTON6_CLICK :
(index == 6) ? BUTTON7_CLICK :
(index == 7) ? BUTTON8_CLICK : BUTTON_ACTION_NONE
);
}
constexpr ButtonAction doubleClick(size_t index) {
return (
(index == 0) ? BUTTON1_DBLCLICK :
(index == 1) ? BUTTON2_DBLCLICK :
(index == 2) ? BUTTON3_DBLCLICK :
(index == 3) ? BUTTON4_DBLCLICK :
(index == 4) ? BUTTON5_DBLCLICK :
(index == 5) ? BUTTON6_DBLCLICK :
(index == 6) ? BUTTON7_DBLCLICK :
(index == 7) ? BUTTON8_DBLCLICK : BUTTON_ACTION_NONE
);
}
constexpr ButtonAction tripleClick(size_t index) {
return (
(index == 0) ? BUTTON1_TRIPLECLICK :
(index == 1) ? BUTTON2_TRIPLECLICK :
(index == 2) ? BUTTON3_TRIPLECLICK :
(index == 3) ? BUTTON4_TRIPLECLICK :
(index == 4) ? BUTTON5_TRIPLECLICK :
(index == 5) ? BUTTON6_TRIPLECLICK :
(index == 6) ? BUTTON7_TRIPLECLICK :
(index == 7) ? BUTTON8_TRIPLECLICK : BUTTON_ACTION_NONE
);
}
constexpr ButtonAction longClick(size_t index) {
return (
(index == 0) ? BUTTON1_LNGCLICK :
(index == 1) ? BUTTON2_LNGCLICK :
(index == 2) ? BUTTON3_LNGCLICK :
(index == 3) ? BUTTON4_LNGCLICK :
(index == 4) ? BUTTON5_LNGCLICK :
(index == 5) ? BUTTON6_LNGCLICK :
(index == 6) ? BUTTON7_LNGCLICK :
(index == 7) ? BUTTON8_LNGCLICK : BUTTON_ACTION_NONE
);
}
constexpr ButtonAction longLongClick(size_t index) {
return (
(index == 0) ? BUTTON1_LNGLNGCLICK :
(index == 1) ? BUTTON2_LNGLNGCLICK :
(index == 2) ? BUTTON3_LNGLNGCLICK :
(index == 3) ? BUTTON4_LNGLNGCLICK :
(index == 4) ? BUTTON5_LNGLNGCLICK :
(index == 5) ? BUTTON6_LNGLNGCLICK :
(index == 6) ? BUTTON7_LNGLNGCLICK :
(index == 7) ? BUTTON8_LNGLNGCLICK : BUTTON_ACTION_NONE
);
}
constexpr size_t relay(size_t index) {
return (
(index == 0) ? (BUTTON1_RELAY - 1) :
(index == 1) ? (BUTTON2_RELAY - 1) :
(index == 2) ? (BUTTON3_RELAY - 1) :
(index == 3) ? (BUTTON4_RELAY - 1) :
(index == 4) ? (BUTTON5_RELAY - 1) :
(index == 5) ? (BUTTON6_RELAY - 1) :
(index == 6) ? (BUTTON7_RELAY - 1) :
(index == 7) ? (BUTTON8_RELAY - 1) : RELAY_NONE
);
}
constexpr unsigned long debounceDelay() {
return BUTTON_DEBOUNCE_DELAY;
}
constexpr unsigned long debounceDelay(size_t index) {
return (
(index == 0) ? BUTTON1_DEBOUNCE_DELAY :
(index == 1) ? BUTTON2_DEBOUNCE_DELAY :
(index == 2) ? BUTTON3_DEBOUNCE_DELAY :
(index == 3) ? BUTTON4_DEBOUNCE_DELAY :
(index == 4) ? BUTTON5_DEBOUNCE_DELAY :
(index == 5) ? BUTTON6_DEBOUNCE_DELAY :
(index == 6) ? BUTTON7_DEBOUNCE_DELAY :
(index == 7) ? BUTTON8_DEBOUNCE_DELAY : debounceDelay()
);
}
constexpr unsigned long repeatDelay() {
return BUTTON_REPEAT_DELAY;
}
constexpr unsigned long repeatDelay(size_t index) {
return (
(index == 0) ? BUTTON1_REPEAT_DELAY :
(index == 1) ? BUTTON2_REPEAT_DELAY :
(index == 2) ? BUTTON3_REPEAT_DELAY :
(index == 3) ? BUTTON4_REPEAT_DELAY :
(index == 4) ? BUTTON5_REPEAT_DELAY :
(index == 5) ? BUTTON6_REPEAT_DELAY :
(index == 6) ? BUTTON7_REPEAT_DELAY :
(index == 7) ? BUTTON8_REPEAT_DELAY : repeatDelay()
);
}
constexpr unsigned long longClickDelay() {
return BUTTON_LNGCLICK_DELAY;
}
constexpr unsigned long longClickDelay(size_t index) {
return (
(index == 0) ? BUTTON1_LNGCLICK_DELAY :
(index == 1) ? BUTTON2_LNGCLICK_DELAY :
(index == 2) ? BUTTON3_LNGCLICK_DELAY :
(index == 3) ? BUTTON4_LNGCLICK_DELAY :
(index == 4) ? BUTTON5_LNGCLICK_DELAY :
(index == 5) ? BUTTON6_LNGCLICK_DELAY :
(index == 6) ? BUTTON7_LNGCLICK_DELAY :
(index == 7) ? BUTTON8_LNGCLICK_DELAY : longClickDelay()
);
}
constexpr unsigned long longLongClickDelay() {
return BUTTON_LNGLNGCLICK_DELAY;
}
constexpr unsigned long longLongClickDelay(size_t index) {
return (
(index == 0) ? BUTTON1_LNGLNGCLICK_DELAY :
(index == 1) ? BUTTON2_LNGLNGCLICK_DELAY :
(index == 2) ? BUTTON3_LNGLNGCLICK_DELAY :
(index == 3) ? BUTTON4_LNGLNGCLICK_DELAY :
(index == 4) ? BUTTON5_LNGLNGCLICK_DELAY :
(index == 5) ? BUTTON6_LNGLNGCLICK_DELAY :
(index == 6) ? BUTTON7_LNGLNGCLICK_DELAY :
(index == 7) ? BUTTON8_LNGLNGCLICK_DELAY : longLongClickDelay()
);
}
constexpr bool mqttSendAllEvents() {
return (1 == BUTTON_MQTT_SEND_ALL_EVENTS);
}
constexpr bool mqttSendAllEvents(size_t index) {
return (
(index == 0) ? (1 == BUTTON1_MQTT_SEND_ALL_EVENTS) :
(index == 1) ? (1 == BUTTON2_MQTT_SEND_ALL_EVENTS) :
(index == 2) ? (1 == BUTTON3_MQTT_SEND_ALL_EVENTS) :
(index == 3) ? (1 == BUTTON4_MQTT_SEND_ALL_EVENTS) :
(index == 4) ? (1 == BUTTON5_MQTT_SEND_ALL_EVENTS) :
(index == 5) ? (1 == BUTTON6_MQTT_SEND_ALL_EVENTS) :
(index == 6) ? (1 == BUTTON7_MQTT_SEND_ALL_EVENTS) :
(index == 7) ? (1 == BUTTON8_MQTT_SEND_ALL_EVENTS) : mqttSendAllEvents()
);
}
constexpr bool mqttRetain() {
return (1 == BUTTON_MQTT_RETAIN);
}
constexpr bool mqttRetain(size_t index) {
return (
(index == 0) ? (1 == BUTTON1_MQTT_RETAIN) :
(index == 1) ? (1 == BUTTON2_MQTT_RETAIN) :
(index == 2) ? (1 == BUTTON3_MQTT_RETAIN) :
(index == 3) ? (1 == BUTTON4_MQTT_RETAIN) :
(index == 4) ? (1 == BUTTON5_MQTT_RETAIN) :
(index == 5) ? (1 == BUTTON6_MQTT_RETAIN) :
(index == 6) ? (1 == BUTTON7_MQTT_RETAIN) :
(index == 7) ? (1 == BUTTON8_MQTT_RETAIN) : mqttRetain()
);
}
constexpr ButtonProvider provider(size_t index) {
return (
(index == 0) ? (BUTTON1_PROVIDER) :
(index == 1) ? (BUTTON2_PROVIDER) :
(index == 2) ? (BUTTON3_PROVIDER) :
(index == 3) ? (BUTTON4_PROVIDER) :
(index == 4) ? (BUTTON5_PROVIDER) :
(index == 5) ? (BUTTON6_PROVIDER) :
(index == 6) ? (BUTTON7_PROVIDER) :
(index == 7) ? (BUTTON8_PROVIDER) : BUTTON_PROVIDER_NONE
);
}
constexpr int analogLevel(size_t index) {
return (
(index == 0) ? (BUTTON1_ANALOG_LEVEL) :
(index == 1) ? (BUTTON2_ANALOG_LEVEL) :
(index == 2) ? (BUTTON3_ANALOG_LEVEL) :
(index == 3) ? (BUTTON4_ANALOG_LEVEL) :
(index == 4) ? (BUTTON5_ANALOG_LEVEL) :
(index == 5) ? (BUTTON6_ANALOG_LEVEL) :
(index == 6) ? (BUTTON7_ANALOG_LEVEL) :
(index == 7) ? (BUTTON8_ANALOG_LEVEL) : 0
);
}
} // namespace build
} // namespace button

+ 42
- 19
code/espurna/compat.h View File

@ -6,17 +6,7 @@ COMPATIBILITY BETWEEN 2.3.0 and latest versions
#pragma once
#include "espurna.h"
// -----------------------------------------------------------------------------
inline constexpr bool isEspurnaCore() {
#if defined(ESPURNA_CORE) || defined(ESPURNA_CORE_WEBUI)
return true;
#else
return false;
#endif
}
#include <Arduino.h>
// -----------------------------------------------------------------------------
// Core version 2.4.2 and higher changed the cont_t structure to a pointer:
@ -87,13 +77,13 @@ long __attribute__((deprecated("Please avoid using map() with Core 2.3.0"))) ma
// Proxy min & max same as the latest Arduino.h
// -----------------------------------------------------------------------------
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
#undef min
#undef max
#undef _min
#undef _max
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
#include <algorithm>
using std::min;
@ -101,33 +91,66 @@ using std::max;
using std::isinf;
using std::isnan;
#define _min(a,b) ({ decltype(a) _a = (a); decltype(b) _b = (b); _a < _b? _a : _b; })
#define _max(a,b) ({ decltype(a) _a = (a); decltype(b) _b = (b); _a > _b? _a : _b; })
#endif
// -----------------------------------------------------------------------------
// std::make_unique & std::clamp backports for C++11, since we still use it
// various backports for C++11, since we still use it with gcc v4.8
// -----------------------------------------------------------------------------
#if __cplusplus <= 201103L
#include <memory>
#include <type_traits>
namespace std {
#if __cplusplus < 202002L
template <typename T>
using remove_cvref = typename std::remove_cv<std::remove_reference<T>>::type;
#endif
#if __cplusplus < 201304L
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
#if __cplusplus < 201603L
template <typename T>
constexpr const T& clamp(const T& value, const T& low, const T& high) {
return (value < low) ? low : (high < value) ? high : value;
}
#endif
} // namespace std
#if __cplusplus < 201411L
template <typename T, size_t Size>
constexpr size_t size(const T (&)[Size]) {
return Size;
}
template <typename T>
constexpr size_t size(const T& value) {
return value.size();
}
template <typename T>
constexpr auto cbegin(const T& value) -> decltype(std::begin(value)) {
return std::begin(value);
}
template <typename T>
constexpr auto cend(const T& value) -> decltype(std::end(value)) {
return std::end(value);
}
#endif
} // namespace std
// Same as min and max, force same type arguments
#undef constrain
// Hijacks useful name
#undef bit
// -----------------------------------------------------------------------------
// Make sure all INPUT modes are available to the source
// (even if those do nothing)


+ 1
- 2
code/espurna/config/all.h View File

@ -30,7 +30,6 @@
#include "custom.h"
#endif
#include "buildtime.h"
#include "version.h"
#include "types.h"
#include "arduino.h"
@ -38,6 +37,6 @@
#include "general.h"
#include "defaults.h"
#include "deprecated.h"
#include "dependencies.h"
#include "sensors.h"
#include "dependencies.h"
#include "webui.h"

+ 122
- 84
code/espurna/config/arduino.h View File

@ -41,13 +41,14 @@
//#define EHOMEDIY_WT02
//#define EHOMEDIY_WT03
//#define ELECTRODRAGON_WIFI_IOT
//#define ESPURNA_CORE
//#define ESPURNA_CORE_WEBUI
//#define ESPURNA_MINIMAL_ARDUINO_OTA
//#define ESPURNA_MINIMAL_WEBUI
//#define ETEKCITY_ESW01_USA
//#define EUROMATE_WIFI_STECKER_SCHUKO
//#define EUROMATE_WIFI_STECKER_SCHUKO_V2
//#define EXS_WIFI_RELAY_V31
//#define EXS_WIFI_RELAY_V50
//#define FCMILA_E27_7W_RGBW
//#define FORNORM_ZLD_34EU
//#define FOXEL_LIGHTFOX_DUAL
//#define FS_UAP1
@ -57,16 +58,18 @@
//#define GENERIC_AG_L4_V3
//#define GENERIC_E14
//#define GENERIC_ECH1560
//#define GENERIC_ESP01_512KB
//#define GENERIC_ESP01S_DHT11_V10
//#define GENERIC_ESP01S_DS18B20_V10
//#define GENERIC_ESP01S_RELAY_V40
//#define GENERIC_ESP01S_RGBLED_V10
//#define GENERIC_ESP01_512KB
//#define GENERIC_GU10
//#define GENERIC_PZEM004T
//#define GENERIC_PZEM004TV30
//#define GENERIC_V9261F
//#define GIZWITS_WITTY_CLOUD
//#define GOSUND_SP111
//#define GOSUND_P1
//#define GOSUND_SP111
//#define GOSUND_WP3
//#define GOSUND_WS1
//#define GREEN_ESP8266RELAY
@ -97,6 +100,7 @@
//#define ITEAD_SONOFF_MINI
//#define ITEAD_SONOFF_POW
//#define ITEAD_SONOFF_POW_R2
//#define ITEAD_SONOFF_POW_R3
//#define ITEAD_SONOFF_RF
//#define ITEAD_SONOFF_RFBRIDGE
//#define ITEAD_SONOFF_S31
@ -122,6 +126,7 @@
//#define LOHAS_E27_9W
//#define LOMBEX_LUX_NOVA2_TUNABLE_WHITE
//#define LOMBEX_LUX_NOVA2_WHITE_COLOR
//#define LSC_E27_10W_WHITE
//#define LSC_SMART_LED_LIGHT_STRIP
//#define LUANI_HVIO
//#define LYASI_LIGHT
@ -132,16 +137,15 @@
//#define MAGICHOME_ZJ_WFMN_A_11
//#define MAGICHOME_ZJ_WFMN_B_11
//#define MAGICHOME_ZJ_WFMN_C_11
//#define MIRABELLA_GENIO_W_A60
//#define MANCAVEMADE_ESPLIVE
//#define MAXCIO_WDE004
//#define MAXCIO_WUK007S
//#define MAXCIO_WUS002S
//#define MIRABELLA_GENIO_W_A60
//#define MUVIT_IO_MIOBULB001
//#define NEDIS_WIFIP310FWT
//#define NEO_COOLCAM_NAS_WR01W
//#define NEXETE_A19
//#define NODEMCU_BASIC
//#define NODEMCU_LOLIN
//#define OPENENERGYMONITOR_MQTT_RELAY
//#define ORVIBO_B25
@ -177,96 +181,130 @@
//#define WORKCHOICE_ECOPLUG
//#define XENON_SM_PW702U
//#define XIAOMI_SMART_DESK_LAMP
//#define YAGUSMART_TOUCH_HWMOD_1G
//#define YAGUSMART_TOUCH_HWMOD_2G
//#define YAGUSMART_TOUCH_HWMOD_3G
//#define YIDIAN_XSSSA05
//#define YJZK_SWITCH_1CH
//#define YJZK_SWITCH_2CH
//#define YJZK_SWITCH_3CH
//#define ZHILDE_44EU_W
//#define ZHILDE_64EU_W
//#define FCMILA_E27_7W_RGBW
//#define LSC_E27_10W_WHITE
//--------------------------------------------------------------------------------
// Features (values below are non-default values)
// Enable features (values below may not be our default values!)
//--------------------------------------------------------------------------------
//#define ALEXA_SUPPORT 0
//#define API_SUPPORT 0
//#define BUTTON_SUPPORT 0
//#define DEBUG_SERIAL_SUPPORT 0
//#define DEBUG_TELNET_SUPPORT 0
//#define DEBUG_UDP_SUPPORT 1
//#define DEBUG_WEB_SUPPORT 0
//#define DOMOTICZ_SUPPORT 0
//#define ENCODER_SUPPORT 1
//#define HOMEASSISTANT_SUPPORT 0
//#define I2C_SUPPORT 1
//#define INFLUXDB_SUPPORT 1
//#define IR_SUPPORT 1
//#define LED_SUPPORT 0
//#define LLMNR_SUPPORT 1
//#define MDNS_SERVER_SUPPORT 0
//#define MQTT_SUPPORT 0
//#define NETBIOS_SUPPORT 1
//#define NOFUSS_SUPPORT 1
//#define NTP_SUPPORT 0
//#define ALEXA_SUPPORT 1
//#define API_SUPPORT 1
//#define BUTTON_PROVIDER_ANALOG_SUPPORT 1
//#define BUTTON_PROVIDER_GPIO_SUPPORT 1
//#define BUTTON_SUPPORT 1
//#define DEBUG_LOG_BUFFER_SUPPORT 1
//#define DEBUG_SERIAL_SUPPORT 1
//#define DEBUG_SUPPORT 1
//#define DEBUG_TELNET_SUPPORT 1
//#define DEBUG_UDP_SUPPORT 1
//#define DEBUG_WEB_SUPPORT 1
//#define DOMOTICZ_SUPPORT 1
//#define ENCODER_SUPPORT 1
//#define GARLAND_SUPPORT 1
//#define HOMEASSISTANT_SUPPORT 1
//#define IFAN_SUPPORT 1
//#define INFLUXDB_SUPPORT 1
//#define IR_RX_SUPPORT 1
//#define IR_SUPPORT 1
//#define IR_TEST_SUPPORT 1
//#define IR_TX_SUPPORT 1
//#define KINGART_CURTAIN_SUPPORT 1
//#define LED_SUPPORT 1
//#define LLMNR_SUPPORT 1
//#define MCP23S08_SUPPORT 1
//#define MDNS_SERVER_SUPPORT 1
//#define MQTT_SUPPORT 1
//#define NETBIOS_SUPPORT 1
//#define NOFUSS_SUPPORT 1
//#define NTP_SUPPORT 1
//#define OTA_ARDUINOOTA_SUPPORT 1
//#define RFM69_SUPPORT 1
//#define RFB_SUPPORT 1
//#define RPN_RULES_SUPPORT 0
//#define SCHEDULER_SUPPORT 0
//#define SPIFFS_SUPPORT 1
//#define SSDP_SUPPORT 1
//#define TELNET_SUPPORT 0
//#define TERMINAL_SUPPORT 0
//#define THINGSPEAK_SUPPORT 0
//#define TUYA_SUPPORT 0
//#define UART_MQTT_SUPPORT 1
//#define WEB_SUPPORT 0
//#define OTA_MQTT_SUPPORT 1
//#define OTA_WEB_SUPPORT 1
//#define PROMETHEUS_SUPPORT 1
//#define PWM_SUPPORT 1
//#define RELAY_PROVIDER_DUAL_SUPPORT 1
//#define RELAY_PROVIDER_STM_SUPPORT 1
//#define RELAY_SUPPORT 1
//#define RFB_SUPPORT 1
//#define RFM69_SUPPORT 1
//#define RPN_RULES_SUPPORT 1
//#define SCHEDULER_SUPPORT 1
//#define SPIFFS_SUPPORT 1
//#define SSDP_SUPPORT 1
//#define TELNET_REVERSE_SUPPORT 1
//#define TELNET_SUPPORT 1
//#define TERMINAL_MQTT_SUPPORT 1
//#define TERMINAL_SERIAL_SUPPORT 1
//#define TERMINAL_SUPPORT 1
//#define TERMINAL_WEB_API_SUPPORT 1
//#define THERMOSTAT_DISPLAY_SUPPORT 1
//#define THERMOSTAT_SUPPORT 1
//#define THINGSPEAK_SUPPORT 1
//#define TUYA_SUPPORT 1
//#define UART_MQTT_SUPPORT 1
//#define UART_SOFTWARE_SUPPORT 1
//#define UART_SUPPORT 1
//#define WEB_SUPPORT 1
//#define WIFI_AP_CAPTIVE_SUPPORT 1
//#define WIFI_GRATUITOUS_ARP_SUPPORT 1
//--------------------------------------------------------------------------------
// Sensors (values below are non-default values)
// Enable sensors (values below may not be our default values!)
//--------------------------------------------------------------------------------
//#define ADE7953_SUPPORT 1
//#define AM2320_SUPPORT 1
//#define ANALOG_SUPPORT 1
//#define BH1750_SUPPORT 1
//#define BMP180_SUPPORT 1
//#define BMX280_SUPPORT 1
//#define BME680_SUPPORT 1
//#define CSE7766_SUPPORT 1
//#define DALLAS_SUPPORT 1
//#define DHT_SUPPORT 1
//#define DIGITAL_SUPPORT 1
//#define ECH1560_SUPPORT 1
//#define EMON_ADC121_SUPPORT 1
//#define EMON_ADS1X15_SUPPORT 1
//#define EMON_ANALOG_SUPPORT 1
//#define EVENTS_SUPPORT 1
//#define EZOPH_SUPPORT 1
//#define GEIGER_SUPPORT 1
//#define GUVAS12SD_SUPPORT 1
//#define HLW8012_SUPPORT 1
//#define LDR_SUPPORT 1
//#define MAX6675_SUPPORT 1
//#define MHZ19_SUPPORT 1
//#define MICS2710_SUPPORT 1
//#define MICS5525_SUPPORT 1
//#define NTC_SUPPORT 1
//#define PMSX003_SUPPORT 1
//#define PULSEMETER_SUPPORT 1
//#define PZEM004T_SUPPORT 1
//#define SDS011_SUPPORT 1
//#define SENSEAIR_SUPPORT 1
//#define SHT3X_I2C_SUPPORT 1
//#define SI7021_SUPPORT 1
//#define SM300D2_SUPPORT 1
//#define SONAR_SUPPORT 1
//#define T6613_SUPPORT 1
//#define THERMOSTAT_SUPPORT 1
//#define TMP3X_SUPPORT 1
//#define V9261F_SUPPORT 1
//#define VEML6075_SUPPORT 1
//#define VL53L1X_SUPPORT 1
//#define HDC1080_SUPPORT 1
//#define ADE7953_SUPPORT 1
//#define AM2320_SUPPORT 1
//#define ANALOG_SUPPORT 1
//#define BH1750_SUPPORT 1
//#define BME680_SUPPORT 1
//#define BMP180_SUPPORT 1
//#define BMX280_SUPPORT 1
//#define CSE7766_SUPPORT 1
//#define DALLAS_SUPPORT 1
//#define DHT_SUPPORT 1
//#define DIGITAL_SUPPORT 1
//#define DUMMY_SENSOR_SUPPORT 1
//#define ECH1560_SUPPORT 1
//#define EMON_ADC121_SUPPORT 1
//#define EMON_ADS1X15_SUPPORT 1
//#define EMON_ANALOG_SUPPORT 1
//#define EVENTS_SUPPORT 1
//#define EZOPH_SUPPORT 1
//#define GEIGER_SUPPORT 1
//#define GUVAS12SD_SUPPORT 1
//#define HDC1080_SUPPORT 1
//#define HLW8012_SUPPORT 1
//#define I2C_SUPPORT 1
//#define INA219_SUPPORT 1
//#define LDR_SUPPORT 1
//#define MAX6675_SUPPORT 1
//#define MHZ19_SUPPORT 1
//#define MICS2710_SUPPORT 1
//#define MICS5525_SUPPORT 1
//#define NTC_SUPPORT 1
//#define PM1006_SUPPORT 1
//#define PMSX003_SUPPORT 1
//#define PULSEMETER_SUPPORT 1
//#define PZEM004TV30_SUPPORT 1
//#define PZEM004T_SUPPORT 1
//#define SDS011_SUPPORT 1
//#define SENSEAIR_SUPPORT 1
//#define SENSOR_SUPPORT 1
//#define SHT3X_I2C_SUPPORT 1
//#define SI1145_SUPPORT 1
//#define SI7021_SUPPORT 1
//#define SM300D2_SUPPORT 1
//#define SONAR_SUPPORT 1
//#define T6613_SUPPORT 1
//#define TMP3X_SUPPORT 1
//#define V9261F_SUPPORT 1
//#define VEML6075_SUPPORT 1
//#define VL53L1X_SUPPORT 1

+ 0
- 97
code/espurna/config/buildtime.h View File

@ -1,97 +0,0 @@
/*
*
* Created: 29.03.2018
*
* Authors:
*
* Assembled from the code released on Stackoverflow by:
* Dennis (instructable.com/member/nqtronix) | https://stackoverflow.com/questions/23032002/c-c-how-to-get-integer-unix-timestamp-of-build-time-not-string
* and
* Alexis Wilke | https://stackoverflow.com/questions/10538444/do-you-know-of-a-c-macro-to-compute-unix-time-and-date
*
* Assembled by Jean Rabault
*
* UNIX_TIMESTAMP gives the UNIX timestamp (unsigned long integer of seconds since 1st Jan 1970) of compilation from macros using the compiler defined __TIME__ macro.
* This should include Gregorian calendar leap days, in particular the 29ths of February, 100 and 400 years modulo leaps.
*
* Careful: __TIME__ is the local time of the computer, NOT the UTC time in general!
*
*/
#ifndef COMPILE_TIME_H_
#define COMPILE_TIME_H_
// Some definitions for calculation
#define SEC_PER_MIN 60UL
#define SEC_PER_HOUR 3600UL
#define SEC_PER_DAY 86400UL
#define SEC_PER_YEAR (SEC_PER_DAY*365)
// extracts 1..4 characters from a string and interprets it as a decimal value
#define CONV_STR2DEC_1(str, i) (str[i]>'0'?str[i]-'0':0)
#define CONV_STR2DEC_2(str, i) (CONV_STR2DEC_1(str, i)*10 + str[i+1]-'0')
#define CONV_STR2DEC_3(str, i) (CONV_STR2DEC_2(str, i)*10 + str[i+2]-'0')
#define CONV_STR2DEC_4(str, i) (CONV_STR2DEC_3(str, i)*10 + str[i+3]-'0')
// Custom "glue logic" to convert the month name to a usable number
#define GET_MONTH(str, i) (str[i]=='J' && str[i+1]=='a' && str[i+2]=='n' ? 1 : \
str[i]=='F' && str[i+1]=='e' && str[i+2]=='b' ? 2 : \
str[i]=='M' && str[i+1]=='a' && str[i+2]=='r' ? 3 : \
str[i]=='A' && str[i+1]=='p' && str[i+2]=='r' ? 4 : \
str[i]=='M' && str[i+1]=='a' && str[i+2]=='y' ? 5 : \
str[i]=='J' && str[i+1]=='u' && str[i+2]=='n' ? 6 : \
str[i]=='J' && str[i+1]=='u' && str[i+2]=='l' ? 7 : \
str[i]=='A' && str[i+1]=='u' && str[i+2]=='g' ? 8 : \
str[i]=='S' && str[i+1]=='e' && str[i+2]=='p' ? 9 : \
str[i]=='O' && str[i+1]=='c' && str[i+2]=='t' ? 10 : \
str[i]=='N' && str[i+1]=='o' && str[i+2]=='v' ? 11 : \
str[i]=='D' && str[i+1]=='e' && str[i+2]=='c' ? 12 : 0)
// extract the information from the time string given by __TIME__ and __DATE__
#define __TIME_SECOND__ (CONV_STR2DEC_2(__TIME__, 6))
#define __TIME_MINUTE__ (CONV_STR2DEC_2(__TIME__, 3))
#define __TIME_HOUR__ (CONV_STR2DEC_2(__TIME__, 0))
#define __TIME_DAY__ (CONV_STR2DEC_2(__DATE__, 4))
#define __TIME_MONTH__ (GET_MONTH(__DATE__, 0))
#define __TIME_YEAR__ (CONV_STR2DEC_4(__DATE__, 7))
// Days in February
#define _UNIX_TIMESTAMP_FDAY(year) \
(((year) % 400) == 0UL ? 29UL : \
(((year) % 100) == 0UL ? 28UL : \
(((year) % 4) == 0UL ? 29UL : \
28UL)))
// Days in the year
#define _UNIX_TIMESTAMP_YDAY(year, month, day) \
( \
/* January */ day \
/* February */ + (month >= 2 ? 31UL : 0UL) \
/* March */ + (month >= 3 ? _UNIX_TIMESTAMP_FDAY(year) : 0UL) \
/* April */ + (month >= 4 ? 31UL : 0UL) \
/* May */ + (month >= 5 ? 30UL : 0UL) \
/* June */ + (month >= 6 ? 31UL : 0UL) \
/* July */ + (month >= 7 ? 30UL : 0UL) \
/* August */ + (month >= 8 ? 31UL : 0UL) \
/* September */+ (month >= 9 ? 31UL : 0UL) \
/* October */ + (month >= 10 ? 30UL : 0UL) \
/* November */ + (month >= 11 ? 31UL : 0UL) \
/* December */ + (month >= 12 ? 30UL : 0UL) \
)
// get the UNIX timestamp from a digits representation
#define _UNIX_TIMESTAMP(year, month, day, hour, minute, second) \
( /* time */ second \
+ minute * SEC_PER_MIN \
+ hour * SEC_PER_HOUR \
+ /* year day (month + day) */ (_UNIX_TIMESTAMP_YDAY(year, month, day) - 1) * SEC_PER_DAY \
+ /* year */ (year - 1970UL) * SEC_PER_YEAR \
+ ((year - 1969UL) / 4UL) * SEC_PER_DAY \
- ((year - 1901UL) / 100UL) * SEC_PER_DAY \
+ ((year - 1601UL) / 400UL) * SEC_PER_DAY \
)
// the UNIX timestamp
#define __UNIX_TIMESTAMP__ (_UNIX_TIMESTAMP(__TIME_YEAR__, __TIME_MONTH__, __TIME_DAY__, __TIME_HOUR__, __TIME_MINUTE__, __TIME_SECOND__))
#endif

+ 34
- 30
code/espurna/config/custom.h.example View File

@ -12,14 +12,14 @@
// Example of a Sonoff Basic board with some options disabled to reduce firmware size.
#if defined(MY_SONOFF_BUILD01) // This section will be used when src_build_flags contains
#if defined(MY_SONOFF_BUILD01) // This section will be used when build_src_flags contains
// -DMY_SONOFF_BUILD01 in 'platformio_override.ini'
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_BASIC_BUILD01"
// Disable these
#define DEBUG_SERIAL_SUPPORT 0
#define UART_SUPPORT 0
#define ALEXA_SUPPORT 0
#define DOMOTICZ_SUPPORT 0
#define HOMEASSISTANT_SUPPORT 0
@ -45,7 +45,7 @@
// the 'DOUBLE CLICK, LONG CLICK OR LONG-LONG CLICK' trigger. A BMX280 environment
// sensor is also connected using I2C on GPIO 1 and 3.
#elif defined(MY_SONOFF_BUILD02) // This section will be used when src_build_flags contains
#elif defined(MY_SONOFF_BUILD02) // This section will be used when build_src_flags contains
// -DMY_SONOFF_BUILD02 in 'platformio_override.ini
// Info - Custom Basic with BMX280 I2C on GPIO 1 and 3
@ -53,44 +53,48 @@
#define DEVICE "SONOFF_BASIC_BMX280" // You can use your own name here
// Disable these
#define DEBUG_SERIAL_SUPPORT 0
#define UART_SUPPORT 0
#define ALEXA_SUPPORT 0
#define DOMOTICZ_SUPPORT 0
#define HOMEASSISTANT_SUPPORT 0
#define THINGSPEAK_SUPPORT 0
// Always publish button events to MQTT
#define BUTTON_MQTT_SEND_ALL_EVENTS 1
// Buttons
#define BUTTON_MQTT_SEND_ALL_EVENTS 1
#define BUTTON1_PIN 0 // Built in button.
#define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
#define BUTTON1_PRESS BUTTON_MODE_NONE
#define BUTTON1_CLICK BUTTON_MODE_TOGGLE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_OFF
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_NONE
#define BUTTON2_PIN 2 // External push button connected between IO2 and GND.
#define BUTTON2_CONFIG BUTTON_PUSHBUTTON | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH
#define BUTTON2_RELAY 1
#define BUTTON2_PRESS BUTTON_MODE_NONE
#define BUTTON2_CLICK BUTTON_MODE_TOGGLE
#define BUTTON2_DBLCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_NONE
#define BUTTON1_PIN 0 // Built in button.
#define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
#define BUTTON1_PRESS BUTTON_MODE_NONE
#define BUTTON1_CLICK BUTTON_MODE_TOGGLE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_OFF
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_NONE
#define BUTTON2_PIN 2 // External push button connected between IO2 and GND.
#define BUTTON2_CONFIG BUTTON_PUSHBUTTON | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH
#define BUTTON2_RELAY 1
#define BUTTON2_PRESS BUTTON_MODE_NONE
#define BUTTON2_CLICK BUTTON_MODE_TOGGLE
#define BUTTON2_DBLCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_NONE
// Relays
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 1
#define LED1_PIN 13
#define LED1_PIN_INVERSE 1
// Non-default I2C on hardware UART0 pins. Make sure to disable UART for these to work!
#define I2C_SDA_PIN 1
#define I2C_SCL_PIN 3
// Extras
#define BMX280_SUPPORT 1
#define BMX280_ADDRESS 0x76
#define I2C_SDA_PIN 1 //TX PIN **DISABLE DEBUG_SERIAL_SUPPORT**
#define I2C_SCL_PIN 3 //RX PIN **DISABLE DEBUG_SERIAL_SUPPORT**
// BMX280 depends on I2C
#define BMX280_SUPPORT 1
#define BMX280_ADDRESS 0x76
#endif

+ 289
- 28
code/espurna/config/defaults.h View File

@ -4,6 +4,179 @@
#pragma once
// **NOTICE** that erasing settings storage will restore to these values.
// make sure these are set to some sane defaults that won't break anything
// -----------------------------------------------------------------------------
// WiFi
// -----------------------------------------------------------------------------
// (required) SSID must be set for network to be considered enabled.
// (optional) PASS with some value will require the network to use WPA (or better) authentication, otherwise it must be an OPEN network.
// (optional) IP, GW, MASK and DNS must all be defined for static configuration to be applied.
// (optional) BSSID and CHANNEL are only used when WIFI_SCAN_NETWORKS is disabled.
#ifndef WIFI1_SSID
#define WIFI1_SSID ""
#endif
#ifndef WIFI1_PASS
#define WIFI1_PASS ""
#endif
#ifndef WIFI1_IP
#define WIFI1_IP ""
#endif
#ifndef WIFI1_GW
#define WIFI1_GW ""
#endif
#ifndef WIFI1_MASK
#define WIFI1_MASK ""
#endif
#ifndef WIFI1_DNS
#define WIFI1_DNS ""
#endif
#ifndef WIFI1_BSSID
#define WIFI1_BSSID ""
#endif
#ifndef WIFI1_CHANNEL
#define WIFI1_CHANNEL 0
#endif
#ifndef WIFI2_SSID
#define WIFI2_SSID ""
#endif
#ifndef WIFI2_PASS
#define WIFI2_PASS ""
#endif
#ifndef WIFI2_IP
#define WIFI2_IP ""
#endif
#ifndef WIFI2_GW
#define WIFI2_GW ""
#endif
#ifndef WIFI2_MASK
#define WIFI2_MASK ""
#endif
#ifndef WIFI2_DNS
#define WIFI2_DNS ""
#endif
#ifndef WIFI2_BSSID
#define WIFI2_BSSID ""
#endif
#ifndef WIFI2_CHANNEL
#define WIFI2_CHANNEL 0
#endif
#ifndef WIFI3_SSID
#define WIFI3_SSID ""
#endif
#ifndef WIFI3_PASS
#define WIFI3_PASS ""
#endif
#ifndef WIFI3_IP
#define WIFI3_IP ""
#endif
#ifndef WIFI3_GW
#define WIFI3_GW ""
#endif
#ifndef WIFI3_MASK
#define WIFI3_MASK ""
#endif
#ifndef WIFI3_DNS
#define WIFI3_DNS ""
#endif
#ifndef WIFI3_BSSID
#define WIFI3_BSSID ""
#endif
#ifndef WIFI3_CHANNEL
#define WIFI3_CHANNEL 0
#endif
#ifndef WIFI4_SSID
#define WIFI4_SSID ""
#endif
#ifndef WIFI4_PASS
#define WIFI4_PASS ""
#endif
#ifndef WIFI4_IP
#define WIFI4_IP ""
#endif
#ifndef WIFI4_GW
#define WIFI4_GW ""
#endif
#ifndef WIFI4_MASK
#define WIFI4_MASK ""
#endif
#ifndef WIFI4_DNS
#define WIFI4_DNS ""
#endif
#ifndef WIFI4_BSSID
#define WIFI4_BSSID ""
#endif
#ifndef WIFI4_CHANNEL
#define WIFI4_CHANNEL 0
#endif
#ifndef WIFI5_SSID
#define WIFI5_SSID ""
#endif
#ifndef WIFI5_PASS
#define WIFI5_PASS ""
#endif
#ifndef WIFI5_IP
#define WIFI5_IP ""
#endif
#ifndef WIFI5_GW
#define WIFI5_GW ""
#endif
#ifndef WIFI5_MASK
#define WIFI5_MASK ""
#endif
#ifndef WIFI5_DNS
#define WIFI5_DNS ""
#endif
#ifndef WIFI5_BSSID
#define WIFI5_BSSID ""
#endif
#ifndef WIFI5_CHANNEL
#define WIFI5_CHANNEL 0
#endif
// -----------------------------------------------------------------------------
// Buttons
// -----------------------------------------------------------------------------
@ -1171,60 +1344,148 @@
// Tuya
// -----------------------------------------------------------------------------
#ifndef TUYA_CH_STATE_DPID
#define TUYA_CH_STATE_DPID 0
#ifndef TUYA_LIGHT_STATE_DPID
#define TUYA_LIGHT_STATE_DPID 0
#endif
#ifndef TUYA_LIGHT_CH1_DPID
#define TUYA_LIGHT_CH1_DPID 0
#endif
#ifndef TUYA_LIGHT_CH2_DPID
#define TUYA_LIGHT_CH2_DPID 0
#endif
#ifndef TUYA_LIGHT_CH3_DPID
#define TUYA_LIGHT_CH3_DPID 0
#endif
#ifndef TUYA_LIGHT_CH4_DPID
#define TUYA_LIGHT_CH4_DPID 0
#endif
#ifndef TUYA_LIGHT_CH5_DPID
#define TUYA_LIGHT_CH5_DPID 0
#endif
#ifndef TUYA_RELAY1_DPID
#define TUYA_RELAY1_DPID 0
#endif
#ifndef TUYA_RELAY2_DPID
#define TUYA_RELAY2_DPID 0
#endif
#ifndef TUYA_RELAY3_DPID
#define TUYA_RELAY3_DPID 0
#endif
#ifndef TUYA_RELAY4_DPID
#define TUYA_RELAY4_DPID 0
#endif
#ifndef TUYA_RELAY5_DPID
#define TUYA_RELAY5_DPID 0
#endif
#ifndef TUYA_RELAY6_DPID
#define TUYA_RELAY6_DPID 0
#endif
#ifndef TUYA_RELAY7_DPID
#define TUYA_RELAY7_DPID 0
#endif
#ifndef TUYA_RELAY8_DPID
#define TUYA_RELAY8_DPID 0
#endif
// -----------------------------------------------------------------------------
// UART
// -----------------------------------------------------------------------------
#ifndef UART1_BAUDRATE
#define UART1_BAUDRATE 115200
#endif
#ifndef UART1_TX_PIN
#define UART1_TX_PIN 1
#endif
#ifndef UART1_RX_PIN
#define UART1_RX_PIN 3
#endif
#ifndef UART1_DATA_BITS
#define UART1_DATA_BITS 8
#endif
#ifndef UART1_PARITY
#define UART1_PARITY None
#endif
#ifndef UART1_STOP_BITS
#define UART1_STOP_BITS 1
#endif
#ifndef UART1_INVERT
#define UART1_INVERT 0
#endif
#ifndef UART2_BAUDRATE
#define UART2_BAUDRATE 115200
#endif
#ifndef TUYA_CH1_DPID
#define TUYA_CH1_DPID 0
#ifndef UART2_TX_PIN
#define UART2_TX_PIN GPIO_NONE
#endif
#ifndef TUYA_CH2_DPID
#define TUYA_CH2_DPID 0
#ifndef UART2_RX_PIN
#define UART2_RX_PIN GPIO_NONE
#endif
#ifndef TUYA_CH3_DPID
#define TUYA_CH3_DPID 0
#ifndef UART2_DATA_BITS
#define UART2_DATA_BITS 8
#endif
#ifndef TUYA_CH4_DPID
#define TUYA_CH4_DPID 0
#ifndef UART2_PARITY
#define UART2_PARITY None
#endif
#ifndef TUYA_CH5_DPID
#define TUYA_CH5_DPID 0
#ifndef UART2_STOP_BITS
#define UART2_STOP_BITS 1
#endif
#ifndef TUYA_SW1_DPID
#define TUYA_SW1_DPID 0
#ifndef UART2_INVERT
#define UART2_INVERT 0
#endif
#ifndef TUYA_SW2_DPID
#define TUYA_SW2_DPID 0
#ifndef UART3_BAUDRATE
#define UART3_BAUDRATE 115200
#endif
#ifndef TUYA_SW3_DPID
#define TUYA_SW3_DPID 0
#ifndef UART3_TX_PIN
#define UART3_TX_PIN GPIO_NONE
#endif
#ifndef TUYA_SW4_DPID
#define TUYA_SW4_DPID 0
#ifndef UART3_RX_PIN
#define UART3_RX_PIN GPIO_NONE
#endif
#ifndef TUYA_SW5_DPID
#define TUYA_SW5_DPID 0
#ifndef UART3_DATA_BITS
#define UART3_DATA_BITS 8
#endif
#ifndef TUYA_SW6_DPID
#define TUYA_SW6_DPID 0
#ifndef UART3_PARITY
#define UART3_PARITY None
#endif
#ifndef TUYA_SW7_DPID
#define TUYA_SW7_DPID 0
#ifndef UART3_STOP_BITS
#define UART3_STOP_BITS 1
#endif
#ifndef TUYA_SW8_DPID
#define TUYA_SW8_DPID 0
#ifndef UART3_INVERT
#define UART3_INVERT 0
#endif
// -----------------------------------------------------------------------------


+ 88
- 4
code/espurna/config/dependencies.h View File

@ -30,14 +30,34 @@
#if UART_MQTT_SUPPORT
#undef MQTT_SUPPORT
#define MQTT_SUPPORT 1 // UART<->MQTT requires MQTT and no serial debug
#define MQTT_SUPPORT 1 // UART<->MQTT requires MQTT and no serial debug & terminal
#undef UART_SUPPORT
#define UART_SUPPORT 1
#undef DEBUG_SERIAL_SUPPORT
#define DEBUG_SERIAL_SUPPORT 0 // TODO: compare UART_MQTT_PORT with DEBUG_PORT? (as strings)
#define DEBUG_SERIAL_SUPPORT 0
#undef TERMINAL_SERIAL_SUPPORT
#define TERMINAL_SERIAL_SUPPORT 0
#endif
#if RELAY_PROVIDER_STM_SUPPORT || RELAY_PROVIDER_DUAL_SUPPORT
#undef UART_SUPPORT
#define UART_SUPPORT 1
#undef DEBUG_SERIAL_SUPPORT
#define DEBUG_SERIAL_SUPPORT 0
#undef TERMINAL_SERIAL_SUPPORT
#define TERMINAL_SERIAL_SUPPORT 0
#endif
#if not UART_SUPPORT
#undef DEBUG_SERIAL_SUPPORT
#define DEBUG_SERIAL_SUPPORT 0
#undef TERMINAL_SERIAL_SUPPORT
#define TERMINAL_SERIAL_SUPPORT 0
#endif
#if ALEXA_SUPPORT
#undef RELAY_SUPPORT
#define RELAY_SUPPORT 1 // and switches
#define RELAY_SUPPORT 1 // alexa needs some switches support to work
#endif
#if RPN_RULES_SUPPORT
@ -99,7 +119,12 @@
#if IFAN_SUPPORT
#undef RELAY_SUPPORT
#define RELAY_SUPPORT 1 // Need relays to manage general state
#define RELAY_SUPPORT 1 // Need relays to manage general state
#endif
#if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
#undef PWM_SUPPORT
#define PWM_SUPPORT 1 // Need PWM to update channel values
#endif
//------------------------------------------------------------------------------
@ -191,3 +216,62 @@
#undef ADC_MODE_VALUE
#define ADC_MODE_VALUE ADC_TOUT
#endif
//------------------------------------------------------------------------------
// RFBRIDGE EFM provider needs serial support
#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
#undef UART_SUPPORT
#define UART_SUPPORT 1
#endif
//------------------------------------------------------------------------------
// Forcibly disable UART logger and terminal, these modules usually
// are expecting to work with the port exclusivelly
// (an so we could more easily describe things in hardware .h)
#if (\
((CSE7766_SUPPORT) && (CSE7766_PORT == DEBUG_SERIAL_PORT)) || \
((EZOPH_SUPPORT) && (EZOPH_PORT == DEBUG_SERIAL_PORT)) || \
((KINGART_CURTAIN_SUPPORT) && (KINGART_CURTAIN_PORT == DEBUG_SERIAL_PORT)) || \
((MHZ19_SUPPORT) && (MHZ19_PORT == DEBUG_SERIAL_PORT)) || \
((PM1006_SUPPORT) && (PM1006_PORT == DEBUG_SERIAL_PORT)) || \
((PMSX003_SUPPORT) && (PMSX003_PORT == DEBUG_SERIAL_PORT)) || \
((PZEM004TV30_SUPPORT) && (PZEM004TV30_PORT == DEBUG_SERIAL_PORT)) || \
((PZEM004T_SUPPORT) && (PZEM004T_PORT == DEBUG_SERIAL_PORT)) || \
((RELAY_PROVIDER_DUAL_SUPPORT) && (RELAY_PROVIDER_DUAL_PORT == DEBUG_SERIAL_PORT)) || \
((RELAY_PROVIDER_STM_SUPPORT) && (RELAY_PROVIDER_STM_PORT == DEBUG_SERIAL_PORT)) || \
((RFB_PROVIDER == RFB_PROVIDER_EFM8BB1) && (RFB_PORT == DEBUG_SERIAL_PORT)) || \
((SDS011_SUPPORT) && (SDS011_PORT == DEBUG_SERIAL_PORT)) || \
((SENSEAIR_SUPPORT) && (SENSEAIR_PORT == DEBUG_SERIAL_PORT)) || \
((SM300D2_SUPPORT) && (SM300D2_PORT == DEBUG_SERIAL_PORT)) || \
((T6613_SUPPORT) && (T6613_PORT == DEBUG_SERIAL_PORT)) || \
((TUYA_SUPPORT) && (TUYA_PORT == DEBUG_SERIAL_PORT)) || \
((V9261F_SUPPORT) && (V9261F_PORT == DEBUG_SERIAL_PORT)) || \
(defined(FOXEL_LIGHTFOX_DUAL) && (LIGHTFOX_PORT == DEBUG_SERIAL_PORT)) \
)
#undef DEBUG_SERIAL_SUPPORT
#define DEBUG_SERIAL_SUPPORT 0
#endif
#if (\
((CSE7766_SUPPORT) && (CSE7766_PORT == TERMINAL_SERIAL_PORT)) || \
((EZOPH_SUPPORT) && (EZOPH_PORT == TERMINAL_SERIAL_PORT)) || \
((KINGART_CURTAIN_SUPPORT) && (KINGART_CURTAIN_PORT == TERMINAL_SERIAL_PORT)) || \
((MHZ19_SUPPORT) && (MHZ19_PORT == TERMINAL_SERIAL_PORT)) || \
((PM1006_SUPPORT) && (PM1006_PORT == TERMINAL_SERIAL_PORT)) || \
((PMSX003_SUPPORT) && (PMSX003_PORT == TERMINAL_SERIAL_PORT)) || \
((PZEM004TV30_SUPPORT) && (PZEM004TV30_PORT == TERMINAL_SERIAL_PORT)) || \
((PZEM004T_SUPPORT) && (PZEM004T_PORT == TERMINAL_SERIAL_PORT)) || \
((RELAY_PROVIDER_DUAL_SUPPORT) && (RELAY_PROVIDER_DUAL_PORT == TERMINAL_SERIAL_PORT)) || \
((RELAY_PROVIDER_STM_SUPPORT) && (RELAY_PROVIDER_STM_PORT == TERMINAL_SERIAL_PORT)) || \
((RFB_PROVIDER == RFB_PROVIDER_EFM8BB1) && (RFB_PORT == TERMINAL_SERIAL_PORT)) || \
((SDS011_SUPPORT) && (SDS011_PORT == TERMINAL_SERIAL_PORT)) || \
((SENSEAIR_SUPPORT) && (SENSEAIR_PORT == TERMINAL_SERIAL_PORT)) || \
((SM300D2_SUPPORT) && (SM300D2_PORT == TERMINAL_SERIAL_PORT)) || \
((T6613_SUPPORT) && (T6613_PORT == TERMINAL_SERIAL_PORT)) || \
((TUYA_SUPPORT) && (TUYA_PORT == TERMINAL_SERIAL_PORT)) || \
((V9261F_SUPPORT) && (V9261F_PORT == TERMINAL_SERIAL_PORT)) || \
(defined(FOXEL_LIGHTFOX_DUAL) && (LIGHTFOX_PORT == TERMINAL_SERIAL_PORT)) \
)
#undef TERMINAL_SERIAL_SUPPORT
#define TERMINAL_SERIAL_SUPPORT 0
#endif

+ 73
- 2
code/espurna/config/deprecated.h View File

@ -100,8 +100,8 @@
#endif
#ifdef CSE7766_PIN
#warning "CSE7766_PIN is deprecated! Please use CSE7766_RX_PIN instead"
#define CSE7766_RX_PIN CSE7766_PIN
#warning "CSE7766_PIN is deprecated! Please use UART[1-3]_RX_PIN"
#define UART1_RX_PIN CSE7766_PIN
#endif
#ifdef WIFI_FALLBACK_APMODE
@ -150,3 +150,74 @@
#undef RFB_SUPPORT
#define RFB_SUPPORT RF_SUPPORT
#endif
#ifdef PZEM004T_ADDRESSES
#warning "PZEM004T_ADDRESSES is deprecated! Addresses can be set by using individual flags PZEM004T_ADDRESS_{1,2,3}"
#endif
#if defined(IR_BUTTON_SET) && not defined(IT_RX_PRESET)
#define IR_RX_PRESET IR_BUTTON_SET
#warning "IR_BUTTON_SET was renamed to IR_RX_PRESET"
#endif
#if defined(TEMPERATURE_MIN_CHANGE) \
|| defined(HUMIDITY_MIN_CHANGE) \
|| defined(ENERGY_MAX_CHANGE)
#warning "Global MIN / MAX CHANGE is replaced with per-magnitude settings, please use ${prefix}MinDelta / ${prefix}MaxDelta"
#endif
#ifdef API_REAL_TIME_VALUES
#define SENSOR_REAL_TIME_VALUES API_REAL_TIME_VALUES
#warning "API_REAL_TIME_VALUES is deprecated! Please use SENSOR_REAL_TIME_VALUES"
#endif
#ifdef DEBUG_PORT
#warning "DEBUG_PORT is deprecated! Please set up the appropriate port as DEBUG_SERIAL_PORT=[1-3]"
#endif
#ifdef SERIAL_BAUDRATE
#warning "SERIAL_BAUDRATE is deprecated! Please use UART[1-3]_BAUDRATE"
#define UART1_BAUDRATE SERIAL_BAUDRATE
#endif
#if ( defined(PZEM004TV30_HW_PORT) || \
defined(PZEM004TV30_USE_SOFT) || \
defined(PZEM004T_HW_PORT) || \
defined(PZEM004T_USE_SOFT) || \
defined(PMS_USE_SOFT) \
)
#warning "Software serial can be enabled with UART_SOFTWARE_SUPPORT"
#undef UART_SOFTWARE_SUPPORT
#define UART_SOFTWARE_SUPPORT 1
#endif
#if ( defined(CSE7766_RX_PIN) || \
defined(CSE7766_BAUDRATE) || \
defined(CSE7766_PIN_INVERSE) || \
defined(MHZ19_RX_PIN) || \
defined(MHZ19_TX_PIN) || \
defined(PM1006_RX_PIN) || \
defined(PM1006_BAUDRATE) || \
defined(PMS_RX_PIN) || \
defined(PMS_TX_PIN) || \
defined(PMS_HW_PORT) || \
defined(PZEM004T_RX_PIN) || \
defined(PZEM004T_TX_PIN) || \
defined(PZEM004TV30_RX_PIN) || \
defined(PZEM004TV30_TX_PIN) || \
defined(SDS011_RX_PIN) || \
defined(SDS011_TX_PIN) || \
defined(SENSEAIR_RX_PIN) || \
defined(SENSEAIR_TX_PIN) || \
defined(SM300D2_RX_PIN) || \
defined(SM300D2_TX_PIN) || \
defined(T6613_RX_PIN) || \
defined(T6613_TX_PIN) || \
defined(V9261F_PIN) || \
defined(V9261F_PIN_INVERSE) || \
defined(V9261F_BAUDRATE) || \
defined(EZOPH_RX_PIN) || \
defined(EZOPH_TX_PIN) \
)
#warning "Sensor serial port configuration should be using UART_... (see general.h) and UART[1-3]_... (see defaults.h)"
#endif

+ 275
- 286
code/espurna/config/general.h View File

@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Do not change this file unless you know what you are doing
// To override user configuration, please see custom.h
//------------------------------------------------------------------------------
@ -9,10 +9,6 @@
// GENERAL
//------------------------------------------------------------------------------
#ifndef DEVICE_NAME
#define DEVICE_NAME MANUFACTURER "_" DEVICE // Concatenate both to get a unique device name
#endif
// When defined, ADMIN_PASS must be 8..63 printable ASCII characters. See:
// https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution)
// https://github.com/xoseperez/espurna/issues/1151
@ -25,11 +21,11 @@
#endif
#ifndef LOOP_DELAY_TIME
#define LOOP_DELAY_TIME 10 // Delay for the main loop, in millis [0-250]
// Recommended minimum is 10, see:
// https://github.com/xoseperez/espurna/issues/1541
// https://github.com/xoseperez/espurna/issues/1631
// https://github.com/esp8266/Arduino/issues/5825
#define LOOP_DELAY_TIME 10 // Time (in milliseconds) to wait every application loop
// This value is clamped between 10 and 250 (ms), ref.
// - https://github.com/xoseperez/espurna/issues/1541
// - https://github.com/xoseperez/espurna/issues/1631
// - https://github.com/esp8266/Arduino/issues/5825
#endif
//------------------------------------------------------------------------------
@ -41,7 +37,7 @@
#endif
#ifndef HEARTBEAT_INTERVAL
#define HEARTBEAT_INTERVAL 300 // Interval between heartbeat messages
#define HEARTBEAT_INTERVAL 300 // Default time (in seconds) for heartbeat messages
#endif
//------------------------------------------------------------------------------
@ -63,12 +59,8 @@
#define DEBUG_SERIAL_SUPPORT 1 // Enable serial debug log
#endif
#ifndef DEBUG_PORT
#define DEBUG_PORT Serial // Default debugging port
#endif
#ifndef SERIAL_BAUDRATE
#define SERIAL_BAUDRATE 115200 // Default baudrate
#ifndef DEBUG_SERIAL_PORT
#define DEBUG_SERIAL_PORT 1 // Default debugging port
#endif
#ifndef DEBUG_ADD_TIMESTAMP
@ -76,22 +68,6 @@
// (in millis overflowing every 1000 seconds)
#endif
// Second serial port (used for RX)
#ifndef SERIAL_RX_ENABLED
#define SERIAL_RX_ENABLED 0 // Secondary serial port for RX
#endif
#ifndef SERIAL_RX_PORT
#define SERIAL_RX_PORT Serial // This setting is usually defined
// in the hardware.h file for those
// boards that require it
#endif
#ifndef SERIAL_RX_BAUDRATE
#define SERIAL_RX_BAUDRATE 115200 // Default baudrate
#endif
//------------------------------------------------------------------------------
// UDP debug log
@ -167,13 +143,8 @@
#define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients
#endif
#ifndef TELNET_SERVER
#define TELNET_SERVER TELNET_SERVER_ASYNC // Can be either TELNET_SERVER_ASYNC (using ESPAsyncTCP) or TELNET_SERVER_WIFISERVER (using WiFiServer)
#endif
#ifndef TELNET_SERVER_ASYNC_BUFFERED
#define TELNET_SERVER_ASYNC_BUFFERED 1 // Enable buffered output for telnet server (+1Kb)
// Helps to avoid lost data with lwip2 TCP_MSS=536 option
#ifndef TELNET_LINE_BUFFER_SIZE
#define TELNET_LINE_BUFFER_SIZE 256 // Temporary buffer, when data arrives in multiple packets without a new-line
#endif
// Enable this flag to add support for reverse telnet (+800 bytes)
@ -188,11 +159,21 @@
//------------------------------------------------------------------------------
#ifndef TERMINAL_SUPPORT
#define TERMINAL_SUPPORT 1 // Enable terminal commands (0.97Kb)
#define TERMINAL_SUPPORT 1 // Enable terminal commands (0.97Kb)
#endif
#ifndef TERMINAL_SHARED_BUFFER_SIZE
#define TERMINAL_SHARED_BUFFER_SIZE 128 // Maximum size for command line, shared by the WebUI, Telnet and Serial
#ifndef TERMINAL_SERIAL_SUPPORT
#define TERMINAL_SERIAL_SUPPORT 1 // Enable terminal over UART
#endif
#ifndef TERMINAL_SERIAL_PORT
#define TERMINAL_SERIAL_PORT 1 // Use specific port configured as UART#_PORT
// (first port by default)
#endif
#ifndef TERMINAL_SERIAL_BUFFER_SIZE
#define TERMINAL_SERIAL_BUFFER_SIZE 128 // Maximum size for command line received from serial input
// (defaults to the size of the peripheral RX buffer)
#endif
#ifndef TERMINAL_MQTT_SUPPORT
@ -218,11 +199,11 @@
#endif
#ifndef SYSTEM_CHECK_TIME
#define SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis
#define SYSTEM_CHECK_TIME 60 // The system is considered stable after these many seconds
#endif
#ifndef SYSTEM_CHECK_MAX
#define SYSTEM_CHECK_MAX 5 // After this many crashes on boot
#define SYSTEM_CHECK_MAX 3 // After this many crashes on boot
// the system is flagged as unstable
#endif
@ -251,8 +232,8 @@
#define GARLAND_SUPPORT 0
#endif
#ifndef GARLAND_D_PIN
#define GARLAND_D_PIN 4 // WS2812 pin number (default: D2 / GPIO4)
#ifndef GARLAND_DATA_PIN
#define GARLAND_DATA_PIN 4 // WS2812 data pin (default: D2 / GPIO4)
#endif
#ifndef GARLAND_LEDS
@ -375,7 +356,7 @@
//------------------------------------------------------------------------------
#ifndef LOADAVG_INTERVAL
#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms)
#define LOADAVG_INTERVAL 30 // Time (in seconds) between load average calculations
#endif
//------------------------------------------------------------------------------
@ -392,11 +373,19 @@
#define RELAY_PROVIDER_STM_SUPPORT 0
#endif
#ifndef RELAY_PROVIDER_STM_PORT
#define RELAY_PROVIDER_STM_PORT 1
#endif
// Sonoff Dual, using serial protocol
#ifndef RELAY_PROVIDER_DUAL_SUPPORT
#define RELAY_PROVIDER_DUAL_SUPPORT 0
#endif
#ifndef RELAY_PROVIDER_DUAL_PORT
#define RELAY_PROVIDER_DUAL_PORT 1
#endif
// Default boot mode: 0 means OFF, 1 ON and 2 whatever was before
#ifndef RELAY_BOOT_MODE
#define RELAY_BOOT_MODE RELAY_BOOT_OFF
@ -408,8 +397,8 @@
#define RELAY_SYNC RELAY_SYNC_ANY
#endif
// 0 (ms) means EVERY relay switches as soon as possible
// otherwise, wait up until this much time before changing the status
// Time (in ms) to wait between relay state changes.
// Setting to zero (default) will cause relay switches to change as soon as possible
#ifndef RELAY_DELAY_INTERLOCK
#define RELAY_DELAY_INTERLOCK 0
#endif
@ -419,27 +408,27 @@
#define RELAY_PULSE_MODE RELAY_PULSE_NONE
#endif
// Default pulse time in seconds
// Default time (in seconds) for the pulse delay, when it is not specified in settings
#ifndef RELAY_PULSE_TIME
#define RELAY_PULSE_TIME 0.0
#endif
// Relay requests flood protection window - in seconds
// Time (in seconds) for the relay flood protection window
#ifndef RELAY_FLOOD_WINDOW
#define RELAY_FLOOD_WINDOW 3.0
#endif
// Allowed actual relay changes inside requests flood protection window
// Maximum amount of relay state changes allowed in the relay flood window
#ifndef RELAY_FLOOD_CHANGES
#define RELAY_FLOOD_CHANGES 5
#endif
// Pulse with in milliseconds for a latched relay
// Time (in ms) for the latched relay pulse
#ifndef RELAY_LATCHING_PULSE
#define RELAY_LATCHING_PULSE 10
#endif
// Do not save relay state after these many milliseconds
// Time (in ms) to wait until saving the relay(s) state
#ifndef RELAY_SAVE_DELAY
#define RELAY_SAVE_DELAY 1000
#endif
@ -575,7 +564,7 @@
#ifndef WIFI_FALLBACK_TIMEOUT
#define WIFI_FALLBACK_TIMEOUT 60000 // When AP is in FALLBACK mode and STA is connected,
// how long to wait until stopping the AP
// how long to wait (in ms) until stopping the AP
#endif
#ifndef WIFI_AP_SSID
@ -588,21 +577,23 @@
// By default or when empty, admin password is used instead.
#endif
#ifndef WIFI_AP_LEASES_SUPPORT
#define WIFI_AP_LEASES_SUPPORT 0 // (optional) Specify softAp MAC<->IP DHCP reservations
// Use `set wifiApLease# MAC`, where MAC is a valid 12-byte HEX number without colons
#endif
#ifndef WIFI_AP_CHANNEL
#define WIFI_AP_CHANNEL 1
#define WIFI_AP_CHANNEL 1 // Which WiFi channel to use for softAP.
#endif
#ifndef WIFI_SLEEP_MODE
#define WIFI_SLEEP_MODE WIFI_NONE_SLEEP // WIFI_NONE_SLEEP, WIFI_LIGHT_SLEEP or WIFI_MODEM_SLEEP
#define WIFI_SLEEP_MODE WIFI_SLEEP_MODE_NONE // WIFI_SLEEP_MODE_NONE - disable all WiFi passive power saving modes (default)
// WIFI_SLEEP_MODE_MODEM - allow WiFi modem to periodially sleep, based on DTIM
// beacon interval time (usually between .1s and 1s)
// WIFI_SLEEP_MODE_LIGHT - in addition to the MODEM sleep also allow CPU to sleep
// between .5s and 3s (varies, depends on active timers)
//
// (ref. https://www.espressif.com/sites/default/files/9b-esp8266-low_power_solutions_en_0.pdf)
#endif
#ifndef WIFI_SCAN_NETWORKS
#define WIFI_SCAN_NETWORKS 1 // Perform a network scan before connecting and when RSSI threshold is reached
// Configured networks are used in order.
#endif
#ifndef WIFI_SCAN_RSSI_THRESHOLD
@ -618,129 +609,6 @@
#define WIFI_SCAN_RSSI_CHECK_INTERVAL 60000 // Time (ms) between RSSI checks
#endif
// Optional hardcoded configuration
// NOTICE that these values become factory-defaults
#ifndef WIFI1_SSID
#define WIFI1_SSID ""
#endif
#ifndef WIFI1_PASS
#define WIFI1_PASS ""
#endif
#ifndef WIFI1_IP
#define WIFI1_IP ""
#endif
#ifndef WIFI1_GW
#define WIFI1_GW ""
#endif
#ifndef WIFI1_MASK
#define WIFI1_MASK ""
#endif
#ifndef WIFI1_DNS
#define WIFI1_DNS ""
#endif
#ifndef WIFI2_SSID
#define WIFI2_SSID ""
#endif
#ifndef WIFI2_PASS
#define WIFI2_PASS ""
#endif
#ifndef WIFI2_IP
#define WIFI2_IP ""
#endif
#ifndef WIFI2_GW
#define WIFI2_GW ""
#endif
#ifndef WIFI2_MASK
#define WIFI2_MASK ""
#endif
#ifndef WIFI2_DNS
#define WIFI2_DNS ""
#endif
#ifndef WIFI3_SSID
#define WIFI3_SSID ""
#endif
#ifndef WIFI3_PASS
#define WIFI3_PASS ""
#endif
#ifndef WIFI3_IP
#define WIFI3_IP ""
#endif
#ifndef WIFI3_GW
#define WIFI3_GW ""
#endif
#ifndef WIFI3_MASK
#define WIFI3_MASK ""
#endif
#ifndef WIFI3_DNS
#define WIFI3_DNS ""
#endif
#ifndef WIFI4_SSID
#define WIFI4_SSID ""
#endif
#ifndef WIFI4_PASS
#define WIFI4_PASS ""
#endif
#ifndef WIFI4_IP
#define WIFI4_IP ""
#endif
#ifndef WIFI4_GW
#define WIFI4_GW ""
#endif
#ifndef WIFI4_MASK
#define WIFI4_MASK ""
#endif
#ifndef WIFI4_DNS
#define WIFI4_DNS ""
#endif
#ifndef WIFI5_SSID
#define WIFI5_SSID ""
#endif
#ifndef WIFI5_PASS
#define WIFI5_PASS ""
#endif
#ifndef WIFI5_IP
#define WIFI5_IP ""
#endif
#ifndef WIFI5_GW
#define WIFI5_GW ""
#endif
#ifndef WIFI5_MASK
#define WIFI5_MASK ""
#endif
#ifndef WIFI5_DNS
#define WIFI5_DNS ""
#endif
// ref: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html#config-lwip-esp-gratuitous-arp
// ref: https://github.com/xoseperez/espurna/pull/1877#issuecomment-525612546
//
@ -768,6 +636,9 @@
#define WIFI_OUTPUT_POWER_DBM 20.0f
#endif
#ifndef WIFI_BOOT_MODE
#define WIFI_BOOT_MODE WIFI_ENABLED
#endif
// -----------------------------------------------------------------------------
// WEB
@ -828,11 +699,11 @@
#endif
#ifndef WS_TIMEOUT
#define WS_TIMEOUT 1800000 // Timeout for secured websocket
#define WS_TIMEOUT 1800 // Time (in seconds) to persist the client session info
#endif
#ifndef WS_UPDATE_INTERVAL
#define WS_UPDATE_INTERVAL 30000 // Update clients every 30 seconds
#define WS_UPDATE_INTERVAL 30 // Time (in seconds) between periodic status updates sent out to every client
#endif
// -----------------------------------------------------------------------------
@ -865,10 +736,6 @@
#define API_BASE_PATH "/api/"
#endif
#ifndef API_REAL_TIME_VALUES
#define API_REAL_TIME_VALUES 0 // Show filtered/median values by default (0 => median, 1 => real time)
#endif
// -----------------------------------------------------------------------------
// MDNS / LLMNR / NETBIOS / SSDP
// -----------------------------------------------------------------------------
@ -909,12 +776,10 @@
// -----------------------------------------------------------------------------
#ifndef SECURE_CLIENT
#define SECURE_CLIENT SECURE_CLIENT_NONE // What variant of WiFiClient to use
#define SECURE_CLIENT SECURE_CLIENT_NONE // What variant of WiFiClient to use:
// SECURE_CLIENT_NONE - No secure client support (default)
// SECURE_CLIENT_AXTLS - axTLS client secure support (All Core versions, ONLY TLS 1.1)
// SECURE_CLIENT_BEARSSL - BearSSL client secure support (starting with 2.5.0, TLS 1.2)
//
// axTLS marked for derecation since Arduino Core 2.4.2 and **will** be removed in the future
// SECURE_CLIENT_BEARSSL - BearSSL client secure support (with Core versions newer than 2.5.0, TLS 1.2)
// SECURE_CLIENT_AXTLS - axTLS client secure support (only with Core versions older than 3.0.0, ONLY TLS 1.1, NOT recommended)
#endif
// Security check that is performed when the connection is established:
@ -971,7 +836,7 @@
#endif
#ifndef OTA_WEB_SUPPORT
#define OTA_WEB_SUPPORT 1 // Support `/upgrade` endpoint and WebUI OTA handler
#define OTA_WEB_SUPPORT WEB_SUPPORT // Support `/upgrade` endpoint and WebUI OTA handler
#endif
#define OTA_GITHUB_FP "CA:06:F5:6B:25:8B:7A:0D:4F:2B:05:47:09:39:47:86:51:15:19:84"
@ -1019,40 +884,56 @@
#define NOFUSS_INTERVAL 3600000 // Check for updates every hour
#endif
// -----------------------------------------------------------------------------
// UART
// -----------------------------------------------------------------------------
#ifndef UART_SUPPORT
#define UART_SUPPORT 1
#endif
#ifndef UART_SOFTWARE_SUPPORT
#define UART_SOFTWARE_SUPPORT 0 // (optional) allows to set TX and RX pins
// to values other than just 1, 2, 3, 13 or 15
// which by default use hardware UART
#endif
// -----------------------------------------------------------------------------
// UART <-> MQTT
// -----------------------------------------------------------------------------
#ifndef UART_MQTT_SUPPORT
#define UART_MQTT_SUPPORT 0 // No support by default
#define UART_MQTT_SUPPORT 0 // Not enabled by default
#endif
#ifndef UART_MQTT_USE_SOFT
#define UART_MQTT_USE_SOFT 0 // Use SoftwareSerial
#ifndef UART_MQTT_PORT
#define UART_MQTT_PORT 1
#endif
#ifndef UART_MQTT_HW_PORT
#define UART_MQTT_HW_PORT Serial // Hardware serial port (if UART_MQTT_USE_SOFT == 0)
#ifndef UART_MQTT_BUFFER_SIZE
#define UART_MQTT_BUFFER_SIZE 128 // Buffen up to N bytes when reading.
// If buffer fills up before we are able to send, old data gets discarded
#endif
#ifndef UART_MQTT_RX_PIN
#define UART_MQTT_RX_PIN 4 // RX PIN (if UART_MQTT_USE_SOFT == 1)
#ifndef UART_MQTT_TERMINATE_OUT
#define UART_MQTT_TERMINATE_OUT '\n' // Write this byte after every received payload
// Set to `\0` to disable
#endif
#ifndef UART_MQTT_TX_PIN
#define UART_MQTT_TX_PIN 5 // TX PIN (if UART_MQTT_USE_SOFT == 1)
#ifndef UART_MQTT_TERMINATE_IN
#define UART_MQTT_TERMINATE_IN '\n' // Read and buffer serial until this byte is found
// Set to `\0` to disable; instead, data will be sent periodically (as soon as it is read)
#endif
#ifndef UART_MQTT_BAUDRATE
#define UART_MQTT_BAUDRATE 115200 // Serial speed
#ifndef UART_MQTT_ENCODE
#define UART_MQTT_ENCODE 0 // Data read from serial will be sent as HEX strings
// (e.g. for binary protocols; 2char per 1byte)
#endif
#ifndef UART_MQTT_TERMINATION
#define UART_MQTT_TERMINATION '\n' // Termination character
#ifndef UART_MQTT_DECODE
#define UART_MQTT_DECODE 0 // MQTT payload received will be converted from HEX before writing to serial
#endif
#define UART_MQTT_BUFFER_SIZE 100 // UART buffer size
// -----------------------------------------------------------------------------
// MQTT
// -----------------------------------------------------------------------------
@ -1079,14 +960,8 @@
//
// Current version of MQTT_LIBRARY_ASYNCMQTTCLIENT only supports SECURE_CLIENT_AXTLS
//
// It is recommended to use WEB_SUPPORT=0 with either SECURE_CLIENT option, as there are miscellaneous problems when using them simultaneously
// (although, things might've improved, and I'd encourage to check whether this is true or not)
//
// When using MQTT_LIBRARY_PUBSUBCLIENT or MQTT_LIBRARY_ARDUINOMQTT, you will have to disable every module that uses ESPAsyncTCP:
// ALEXA_SUPPORT=0, INFLUXDB_SUPPORT=0, TELNET_SUPPORT=0, THINGSPEAK_SUPPORT=0, DEBUG_TELNET_SUPPORT=0 and WEB_SUPPORT=0
// Or, use "sync" versions instead (note that not every module has this option):
// THINGSPEAK_USE_ASYNC=0, TELNET_SERVER=TELNET_SERVER_WIFISERVER
//
// It is recommended to use as little modules as possible with SECURE_CLIENT option, as it requires a large amount
// of pre-allocated RAM. Even in MFLN mode, it will still require at least 16KiB to prepare input buffer.
// See SECURE_CLIENT_CHECK for all possible connection verification options.
//
// The simpliest way to verify SSL connection is to use fingerprinting.
@ -1152,6 +1027,8 @@
#ifndef MQTT_QOS
#define MQTT_QOS 0 // MQTT QoS value for all messages
// ! Neither MQTT library supports QoS > 0 properly at this time.
// ! PUB ACKs checks, re-publishing, etc. is missing
#endif
#ifndef MQTT_KEEPALIVE
@ -1242,8 +1119,6 @@
#define SETTINGS_AUTOSAVE 1 // Autosave settings or force manual commit
#endif
#define SETTINGS_MAX_LIST_COUNT 16 // Maximum index for settings lists
// -----------------------------------------------------------------------------
// LIGHT
// -----------------------------------------------------------------------------
@ -1259,7 +1134,7 @@
// 5 channels => RGBWW
#ifndef LIGHT_PROVIDER
#define LIGHT_PROVIDER LIGHT_PROVIDER_NONE
#define LIGHT_PROVIDER LIGHT_PROVIDER_NONE
#endif
#ifndef LIGHT_REPORT_DELAY
@ -1274,26 +1149,6 @@
#define LIGHT_SAVE_DELAY 5000 // Persist channel & brightness values after the specified number of ms
#endif
#ifndef LIGHT_MIN_PWM
#define LIGHT_MIN_PWM 0
#endif
#ifndef LIGHT_MAX_PWM
#if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
#define LIGHT_MAX_PWM 255
#elif LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
#define LIGHT_MAX_PWM 10000 // 10000 * 200ns => 2 kHz
#else
#define LIGHT_MAX_PWM 0
#endif
#endif // LIGHT_MAX_PWM
#ifndef LIGHT_LIMIT_PWM
#define LIGHT_LIMIT_PWM LIGHT_MAX_PWM // Limit PWM to this value (prevent 100% power)
#endif
#ifndef LIGHT_MIN_VALUE
#define LIGHT_MIN_VALUE 0 // Minimum light value
#endif
@ -1313,7 +1168,7 @@
// Default mireds & kelvin to the Philips Hue limits
// https://developers.meethue.com/documentation/core-concepts
//
// Home Assistant also uses these, see Light::min_mireds, Light::max_mireds
// Home Assistant also uses these, see espurna::light::{min,max}_mireds
// https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/light/__init__.py
// Used when LIGHT_USE_WHITE AND LIGHT_USE_CCT is 1 - (1000000/Kelvin = MiReds)
@ -1368,10 +1223,6 @@
#define LIGHT_TRANSITION_TIME 500 // Time in millis from color to color
#endif
#ifndef LIGHT_RELAY_ENABLED
#define LIGHT_RELAY_ENABLED 1 // Add a virtual switch that controls the global light state. Depends on RELAY_SUPPORT
#endif
// -----------------------------------------------------------------------------
// DOMOTICZ
// -----------------------------------------------------------------------------
@ -1503,11 +1354,16 @@
#endif // ifndef THINGSPEAK_ADDRESS
#ifndef THINGSPEAK_TRIES
#define THINGSPEAK_TRIES 3 // Number of tries when sending data (minimum 1)
#define THINGSPEAK_TRIES 3 // Number of attemps to send the data (minimum 1)
#endif
#define THINGSPEAK_MIN_INTERVAL 15000 // Minimum interval between POSTs (in millis)
#define THINGSPEAK_FIELDS 8 // Number of fields
#ifndef THINGSPEAK_MIN_INTERVAL
#define THINGSPEAK_MIN_INTERVAL 15000 // Minimum interval between POSTs (milliseconds)
#endif
#ifndef THINGSPEAK_FIELDS
#define THINGSPEAK_FIELDS 8 // Maximum number of fields that will be prepared
#endif
// -----------------------------------------------------------------------------
// SCHEDULER
@ -1518,7 +1374,7 @@
#endif
#ifndef SCHEDULER_MAX_SCHEDULES
#define SCHEDULER_MAX_SCHEDULES 10 // Max schedules alowed
#define SCHEDULER_MAX_SCHEDULES 10 // Max schedules allowed
#endif
#ifndef SCHEDULER_RESTORE_LAST_SCHEDULE
@ -1550,15 +1406,15 @@
// -----------------------------------------------------------------------------
#ifndef NTP_SUPPORT
#define NTP_SUPPORT 1 // Build with NTP support by default (depends on Core version)
#define NTP_SUPPORT 1 // Build with NTP support by default
#endif
#ifndef NTP_SERVER
#define NTP_SERVER "pool.ntp.org" // Default NTP server
#define NTP_SERVER "pool.ntp.org" // Default NTP server (string)
#endif
#ifndef NTP_TIMEZONE
#define NTP_TIMEZONE TZ_Etc_UTC // POSIX TZ variable. Default to UTC from TZ.h (which is PSTR("UTC0"))
#define NTP_TIMEZONE TZ_Etc_UTC // POSIX TZ variable (string). Default to value from, TZ.h which is "UTC0"
// For the format documentation, see:
// - https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
// ESP8266 Core provides human-readable aliases for POSIX format, see:
@ -1569,11 +1425,11 @@
#endif
#ifndef NTP_UPDATE_INTERVAL
#define NTP_UPDATE_INTERVAL 1800 // NTP check every 30 minutes
#define NTP_UPDATE_INTERVAL 1800 // Interval (in seconds) for the periodic NTP update
#endif
#ifndef NTP_START_DELAY
#define NTP_START_DELAY 3 // Delay NTP start for 3 seconds
#define NTP_START_DELAY 3 // Time (in seconds) to delay the first NTP update
#endif
#ifndef NTP_WAIT_FOR_SYNC
@ -1593,24 +1449,20 @@
#define ALEXA_SUPPORT 1 // Enable Alexa support by default (10.84Kb)
#endif
// This is default value for the alexaEnabled setting that defines whether
// this device should be discoberable and respond to Alexa commands.
// Both ALEXA_SUPPORT and alexaEnabled should be 1 for Alexa support to work.
#ifndef ALEXA_ENABLED
#define ALEXA_ENABLED 1
#define ALEXA_ENABLED 1 // Start Alexa HTTP server by default
#endif
#ifndef ALEXA_HOSTNAME
#define ALEXA_HOSTNAME ""
#define ALEXA_HOSTNAME "" // Alexa device name. When this value is empty, HOSTNAME will be used.
#endif
// -----------------------------------------------------------------------------
// RF BRIDGE
// -----------------------------------------------------------------------------
#ifndef RFB_SUPPORT
#define RFB_SUPPORT 0
#define RFB_SUPPORT 0 // Enable RFBridge support (disabled by default)
#endif
#ifndef RFB_SEND_REPEATS
@ -1628,8 +1480,12 @@
#define RFB_PROVIDER RFB_PROVIDER_RCSWITCH
#endif
#ifndef RFB_PORT
#define RFB_PORT 1 // If EFM is enabled, use this UART port
#endif
#ifndef RFB_RX_PIN
#define RFB_RX_PIN GPIO_NONE
#define RFB_RX_PIN GPIO_NONE // If RCSWITCH is enabled, use these pins
#endif
#ifndef RFB_TX_PIN
@ -1657,38 +1513,122 @@
// -----------------------------------------------------------------------------
#ifndef IR_SUPPORT
#define IR_SUPPORT 0 // Do not build with IR support by default (10.25Kb)
#define IR_SUPPORT 0 // (boolean) Do not build with IR support by default
#endif
#ifndef IR_RX_SUPPORT
#define IR_RX_SUPPORT 1 // (boolean) IR receiver support in the build (~30Kb, enabled by default)
#endif
#ifndef IR_RX_PIN
#define IR_RX_PIN GPIO_NONE // GPIO the receiver is connected to
#endif
#ifndef IR_RX_PULLUP
#define IR_RX_PULLUP 0 // (boolean) whether the IR receiver pin is setup with INPUT_PULLUP
#endif
#ifndef IR_TX_SUPPORT
#define IR_TX_SUPPORT 1 // (boolean) IR transmitter support in the build (~8Kb, enabled by default)
#endif
//#define IR_RX_PIN 5 // GPIO the receiver is connected to
//#define IR_TX_PIN 4 // GPIO the transmitter is connected to
#ifndef IR_TX_PIN
#define IR_TX_PIN GPIO_NONE // GPIO the transmitter is connected to
#endif
#ifndef IR_TX_INVERTED
#define IR_TX_INVERTED 0 // By default, turn LED ON when GPIO is HIGH and OFF when it's LOW
#endif
#ifndef IR_USE_RAW
#define IR_USE_RAW 0 // Use raw codes
#ifndef IR_TX_MODULATION
#define IR_TX_MODULATION 1 // (boolean, interanl) enable frequency modulation, enabled by default
#endif
#ifndef IR_BUFFER_SIZE
#define IR_BUFFER_SIZE 1024
#ifndef IR_RX_BUFFER_SIZE
#define IR_RX_BUFFER_SIZE 128 // (ms, internal) size of the buffer that will be used to store the captured data
// required heap amount is the buffer size multiplied by four (default is 512bytes)
#endif
#ifndef IR_TIMEOUT
#define IR_TIMEOUT 15U
#ifndef IR_RX_TIMEOUT
#define IR_RX_TIMEOUT 15 // (ms, internal) amount of time of no IR signal before the library stops capturing the data
#endif
#ifndef IR_REPEAT
#define IR_REPEAT 1
#ifndef IR_RX_SIMPLE_MQTT
#define IR_RX_SIMPLE_MQTT 1 // (boolean) Report simple protocols
#endif
#ifndef IR_DELAY
#define IR_DELAY 100
#ifndef IR_RX_RAW_MQTT
#define IR_RX_RAW_MQTT 0 // (boolean) Report RAW payload for everything received (even unknown protocols)
#endif
#ifndef IR_DEBOUNCE
#define IR_DEBOUNCE 500 // IR debounce time in milliseconds
#ifndef IR_RX_STATE_MQTT
#define IR_RX_STATE_MQTT 0 // (boolean) Report state payload for supported protocols
#endif
#ifndef IR_BUTTON_SET
#define IR_BUTTON_SET 0 // IR button set to use (see ../ir_button.h)
#ifndef IR_RX_SIMPLE_MQTT_TOPIC
#define IR_RX_SIMPLE_MQTT_TOPIC "irin" // (string) MQTT topics are composed as {root}/{topic},
// this one will be used to publish simple protocol messages
// (or, automatically calculated FNV1 hash values when the protocol type is unknown)
#endif
#ifndef IR_TX_SIMPLE_MQTT_TOPIC
#define IR_TX_SIMPLE_MQTT_TOPIC "irout" // (string) MQTT topic subscription to transmit the received message
// (in a simple format)
#endif
#ifndef IR_RX_RAW_MQTT_TOPIC
#define IR_RX_RAW_MQTT_TOPIC "irraw" // (string) MQTT topic to publish the received messages in RAW format
#endif
#ifndef IR_TX_RAW_MQTT_TOPIC
#define IR_TX_RAW_MQTT_TOPIC "irraw" // (string) MQTT topic subscription to transmit the RAW timings
#endif
#ifndef IR_RX_STATE_MQTT_TOPIC
#define IR_RX_STATE_MQTT_TOPIC "irstate" // (string) MQTT topic to publish messages with 'state'
// (commonly, HVAC with payload size >=64bit, but this depends on the protocol)
#endif
#ifndef IR_TX_STATE_MQTT_TOPIC
#define IR_TX_STATE_MQTT_TOPIC "irstate" // (string) MQTT topic subscription to transmit the state messages
#endif
#ifndef IR_TX_REPEATS
#define IR_TX_REPEATS 0 // (number) additional number of times that the message will be sent per series
// (currently, only for simple payloads. *may* be overriden by the protocol or the option)
#endif
#ifndef IR_TX_SERIES
#define IR_TX_SERIES 1 // (number) default number of times that the message will be sent
// (can be overriden in the MQTT payload option for the specific message)
#endif
#ifndef IR_TX_DELAY
#define IR_TX_DELAY 100 // (ms) minimum amount of time to wait before transmitting another message
// (when using series >1, will also wait between the same message)
#endif
#ifndef IR_RX_DELAY
#define IR_RX_DELAY 100 // (ms) minimum amount of time to wait before processing incomming message
#endif
#ifndef IR_RX_PRESET
#define IR_RX_PRESET 0 // (number) IR-code-as-button preset to use
// 0 - disabled
// 1,2,5 - generic remote shipped with the RGB controller
// 3 - Samsung AA59-00608A for a generic 8CH module
// 4 - Remote for a generic 1CH module
// (~1Kb, see ir.cpp for more info about the presets)
#endif
#ifndef IR_RX_UNKNOWN
#define IR_RX_UNKNOWN 1 // (boolean) do not discard unknown (-1) protocols by default
// (*notice* that disabling this will cause RAW output to stop working)
#endif
#ifndef IR_TEST_SUPPORT
#define IR_TEST_SUPPORT 0 // (boolean) enables internal tests and sanity checks that will be called on boot
// (disabled by default and should only be enabled with debug support)
#endif
//--------------------------------------------------------------------------------
@ -1765,8 +1705,8 @@
#define TUYA_SUPPORT 0
#endif
#ifndef TUYA_SERIAL
#define TUYA_SERIAL Serial
#ifndef TUYA_PORT
#define TUYA_PORT 1
#endif
#ifndef TUYA_FILTER_ENABLED
@ -1790,7 +1730,7 @@
//--------------------------------------------------------------------------------
#ifndef PROMETHEUS_SUPPORT
#define PROMETHEUS_SUPPORT 0
#define PROMETHEUS_SUPPORT API_SUPPORT
#endif
//--------------------------------------------------------------------------------
@ -1801,6 +1741,55 @@
#define IFAN_SUPPORT 0
#endif
//--------------------------------------------------------------------------------
// PWM driver support
//--------------------------------------------------------------------------------
#ifndef PWM_SUPPORT
#define PWM_SUPPORT 0
#endif
#ifndef PWM_PROVIDER
#define PWM_PROVIDER PWM_PROVIDER_GENERIC // Currently, two software PWM providers are supported
// - PWM_PROVIDER_GENERIC (default)
// - PWM_PROVIDER_ARDUINO
#endif
#ifndef PWM_FREQUENCY
#define PWM_FREQUENCY 500 // (Hz)
#endif
#ifndef PWM_RESOLUTION
#define PWM_RESOLUTION 10 // (bits)
// Range of raw values accepted by the driver
// 0...1023 by default
#endif
#ifndef PWM_DUTY_LIMIT
#define PWM_DUTY_LIMIT 100 // (percentage)
// Clamp duty value, prevent 100% power
#endif
// =============================================================================
// Curtain hardware support
// =============================================================================
#ifndef KINGART_CURTAIN_SUPPORT
#define KINGART_CURTAIN_SUPPORT 0
#endif
#ifndef KINGART_CURTAIN_PORT
#define KINGART_CURTAIN_PORT 1
#endif
#ifndef KINGART_CURTAIN_BUFFER_SIZE
#define KINGART_CURTAIN_BUFFER_SIZE 128
#endif
#ifndef KINGART_CURTAIN_TERMINATION
#define KINGART_CURTAIN_TERMINATION '\e' // Termination character after each message
#endif
// =============================================================================
// Configuration helpers to help detect features
// =============================================================================


+ 602
- 204
code/espurna/config/hardware.h
File diff suppressed because it is too large
View File


+ 231
- 133
code/espurna/config/sensors.h View File

@ -21,7 +21,7 @@
#endif
#ifndef SENSOR_INIT_INTERVAL
#define SENSOR_INIT_INTERVAL 10000 // Try to re-init non-ready sensors every 10s
#define SENSOR_INIT_INTERVAL 10 // Try to re-init non-ready sensors every 10s
#endif
#ifndef SENSOR_REPORT_EVERY
@ -45,16 +45,8 @@
#define SENSOR_POWER_CHECK_STATUS 1 // If set to 1 the reported power/current/energy will be 0 if the relay[0] is OFF
#endif
#ifndef TEMPERATURE_MIN_CHANGE
#define TEMPERATURE_MIN_CHANGE 0.0 // Minimum temperature change to report
#endif
#ifndef HUMIDITY_MIN_CHANGE
#define HUMIDITY_MIN_CHANGE 0.0 // Minimum humidity change to report
#endif
#ifndef ENERGY_MAX_CHANGE
#define ENERGY_MAX_CHANGE 0.0 // Maximum energy change to report (if >0 it will allways report when delta(E) is greater than this)
#ifndef SENSOR_REAL_TIME_VALUES
#define SENSOR_REAL_TIME_VALUES 0 // Show filtered/median values by default (0 => median, 1 => real time)
#endif
#ifndef SENSOR_SAVE_EVERY
@ -65,9 +57,13 @@
// Warning: this might wear out flash fast!
#endif
#ifndef SENSOR_PUBLISH_ADDRESSES
#define SENSOR_PUBLISH_ADDRESSES 0 // Publish sensor addresses
#define SENSOR_ADDRESS_TOPIC "address" // Topic to publish sensor addresses
#endif
#ifndef SENSOR_ADDRESS_TOPIC
#define SENSOR_ADDRESS_TOPIC "address" // Topic to publish sensor addresses
#endif
// -----------------------------------------------------------------------------
// Magnitude offset correction
@ -156,7 +152,25 @@
#define BH1750_ADDRESS 0x00 // 0x00 means auto
#endif
#ifndef BH1750_ACCURACY
#define BH1750_ACCURACY 1.2 // RAW value conversion ratio
// Allowed values are 0.96...1.44
#endif
#ifndef BH1750_SENSITIVITY
#define BH1750_SENSITIVITY 1.0 // Measurement sensitivity; value is derived from 'MTreg CURRENT'
// `SENSITIVITY = MTreg CURRENT / MTreg DEFAULT` (up to 2 decimal places)
// e.g. for MTreg allowed values of 31...254
// * 31 -> 0.45 (min)
// * 69 -> 1.0
// * 138 -> 2.0
// * 207 -> 3.0
// * 254 -> 3.68 (max)
#endif
#ifndef BH1750_MODE
#define BH1750_MODE BH1750_CONTINUOUS_HIGH_RES_MODE
#endif
//------------------------------------------------------------------------------
// BMP085/BMP180
@ -272,22 +286,11 @@
#define CSE7766_SUPPORT 0
#endif
#ifndef CSE7766_RX_PIN
#define CSE7766_RX_PIN 3 // RX pin connected to the CSE7766
// As we never transmit anything, this is the only pin used
#endif
#ifndef CSE7766_PIN_INVERSE
#define CSE7766_PIN_INVERSE 0 // Signal is inverted
#ifndef CSE7766_PORT
#define CSE7766_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 4800`)
#endif
#define CSE7766_SYNC_INTERVAL 300 // Safe time between transmissions (ms)
#define CSE7766_BAUDRATE 4800 // UART baudrate
#define CSE7766_V1R 1.0 // 1mR current resistor
#define CSE7766_V2R 1.0 // 1M voltage resistor
//------------------------------------------------------------------------------
// Digital sensor
// Enable support by passing DIGITAL_SUPPORT=1 build flag
@ -306,7 +309,7 @@
#endif
#ifndef DIGITAL1_DEFAULT_STATE
#define DIGITAL1_DEFAULT_STATE 1
#define DIGITAL1_DEFAULT_STATE HIGH
#endif
#ifndef DIGITAL2_PIN
@ -318,7 +321,7 @@
#endif
#ifndef DIGITAL2_DEFAULT_STATE
#define DIGITAL2_DEFAULT_STATE 1
#define DIGITAL2_DEFAULT_STATE HIGH
#endif
#ifndef DIGITAL3_PIN
@ -330,7 +333,7 @@
#endif
#ifndef DIGITAL3_DEFAULT_STATE
#define DIGITAL3_DEFAULT_STATE 1
#define DIGITAL3_DEFAULT_STATE HIGH
#endif
#ifndef DIGITAL4_PIN
@ -342,7 +345,7 @@
#endif
#ifndef DIGITAL4_DEFAULT_STATE
#define DIGITAL4_DEFAULT_STATE 1
#define DIGITAL4_DEFAULT_STATE HIGH
#endif
#ifndef DIGITAL5_PIN
@ -354,7 +357,7 @@
#endif
#ifndef DIGITAL5_DEFAULT_STATE
#define DIGITAL5_DEFAULT_STATE 1
#define DIGITAL5_DEFAULT_STATE HIGH
#endif
#ifndef DIGITAL6_PIN
@ -366,7 +369,7 @@
#endif
#ifndef DIGITAL6_DEFAULT_STATE
#define DIGITAL6_DEFAULT_STATE 1
#define DIGITAL6_DEFAULT_STATE HIGH
#endif
#ifndef DIGITAL7_PIN
@ -378,7 +381,7 @@
#endif
#ifndef DIGITAL7_DEFAULT_STATE
#define DIGITAL7_DEFAULT_STATE 1
#define DIGITAL7_DEFAULT_STATE HIGH
#endif
#ifndef DIGITAL8_PIN
@ -390,7 +393,16 @@
#endif
#ifndef DIGITAL8_DEFAULT_STATE
#define DIGITAL8_DEFAULT_STATE 1
#define DIGITAL8_DEFAULT_STATE HIGH
#endif
//------------------------------------------------------------------------------
// Dummy sensor, implementing some of the magnitudes
// Enable support by passing DUMMY_SENSOR_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef DUMMY_SENSOR_SUPPORT
#define DUMMY_SENSOR_SUPPORT 0
#endif
//------------------------------------------------------------------------------
@ -508,6 +520,10 @@
#define EMON_ANALOG_SUPPORT 0 // Do not build support by default
#endif
#ifndef EMON_ANALOG_RESOLUTION
#define EMON_ANALOG_RESOLUTION 10 // ADC resolution (in bits)
#endif
//------------------------------------------------------------------------------
// Counter sensor
// Enable support by passing EVENTS_SUPPORT=1 build flag
@ -666,13 +682,23 @@
#define GEIGER_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#define GEIGER_DEBOUNCE 25 // Do not register events within less than 25 millis.
// Value derived here: Debounce time 25ms, because https://github.com/Trickx/espurna/wiki/Geiger-counter
#ifndef GEIGER_DEBOUNCE
#define GEIGER_DEBOUNCE 25 // (milliseconds) Do not register events within less than 25 millis.
#endif
// See https://github.com/Trickx/espurna/wiki/Geiger-counter
#ifndef GEIGER_CPM2SIEVERT
#define GEIGER_CPM2SIEVERT 240 // CPM to µSievert per hour conversion factor
#endif
// Typically the literature uses the invers, but I find an integer type more convienient.
#ifndef GEIGER_REPORT_SIEVERTS
#define GEIGER_REPORT_SIEVERTS 1 // Enabler for local dose rate reports in µSv/h
#endif
#ifndef GEIGER_REPORT_CPM
#define GEIGER_REPORT_CPM 1 // Enabler for local dose rate reports in counts per minute
#endif
//------------------------------------------------------------------------------
// GUVAS12SD UV Sensor (analog)
@ -725,15 +751,15 @@
#endif
#ifndef HLW8012_CURRENT_RATIO
#define HLW8012_CURRENT_RATIO 0.0 // Set to 0.0 to use factory defaults
#define HLW8012_CURRENT_RATIO HLW8012_DEFAULT_CURRENT_RATIO // Value multiplier, internally used to scale RAW current
#endif
#ifndef HLW8012_VOLTAGE_RATIO
#define HLW8012_VOLTAGE_RATIO 0.0 // Set to 0.0 to use factory defaults
#define HLW8012_VOLTAGE_RATIO HLW8012_DEFAULT_VOLTAGE_RATIO // Value multiplier, internally used to scale RAW voltage
#endif
#ifndef HLW8012_POWER_RATIO
#define HLW8012_POWER_RATIO 0.0 // Set to 0.0 to use factory defaults
#define HLW8012_POWER_RATIO HLW8012_DEFAULT_POWER_RATIO // Value multiplier, internally used to scale RAW active power
#endif
#ifndef HLW8012_USE_INTERRUPTS
@ -797,12 +823,17 @@
#define MHZ19_SUPPORT 0
#endif
#ifndef MHZ19_RX_PIN
#define MHZ19_RX_PIN 13
#ifndef MHZ19_PORT
#define MHZ19_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
#ifndef MHZ19_TX_PIN
#define MHZ19_TX_PIN 15
#ifndef MHZ19_CALIBRATE_AUTO
#define MHZ19_CALIBRATE_AUTO 0
#endif
#ifndef MHZ19_DETECTION_RANGE
#define MHZ19_DETECTION_RANGE 2000
#endif
//------------------------------------------------------------------------------
@ -861,12 +892,20 @@
#define NTC_DELAY 0 // Delay between samples in micros
#endif
#ifndef NTC_R_UP
#define NTC_R_UP 0 // Resistor upstream, set to 0 if none
#ifndef NTC_INPUT_VOLTAGE
#define NTC_INPUT_VOLTAGE 3.3 // Actual voltage that is connected to the ADC
#endif
#ifndef NTC_R_DOWN
#define NTC_R_DOWN 10000 // Resistor downstream, set to 0 if none
#define NTC_R_DOWN 10000 // (Ohm) Resistor DOWN, NTC is connected to the voltage
// [V]NTCR_DOWN[GND]
// [ADC]
#endif
#ifndef NTC_R_UP
#define NTC_R_UP 0 // (Ohm) Resistor UP, NTC is connected to the ground
// [V]R_UPNTC[GND]
// [ADC]
#endif
#ifndef NTC_T0
@ -881,6 +920,20 @@
#define NTC_BETA 3977 // Beta coeficient
#endif
//------------------------------------------------------------------------------
// PM1006 sensor
// Enable support by passing PM1006_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef PM1006_SUPPORT
#define PM1006_SUPPORT 0
#endif
#ifndef PM1006_PORT
#define PM1006_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
//------------------------------------------------------------------------------
// Particle Monitor based on Plantower PMS
// Enable support by passing PMSX003_SUPPORT=1 build flag
@ -890,6 +943,11 @@
#define PMSX003_SUPPORT 0
#endif
#ifndef PMS_PORT
#define PMS_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
#ifndef PMS_TYPE
#define PMS_TYPE PMS_TYPE_X003
#endif
@ -901,22 +959,6 @@
#define PMS_SMART_SLEEP 0
#endif
#ifndef PMS_USE_SOFT
#define PMS_USE_SOFT 0 // If PMS_USE_SOFT == 1, DEBUG_SERIAL_SUPPORT must be 0
#endif
#ifndef PMS_RX_PIN
#define PMS_RX_PIN 13 // Software serial RX GPIO (if PMS_USE_SOFT == 1)
#endif
#ifndef PMS_TX_PIN
#define PMS_TX_PIN 15 // Software serial TX GPIO (if PMS_USE_SOFT == 1)
#endif
#ifndef PMS_HW_PORT
#define PMS_HW_PORT Serial // Hardware serial port (if PMS_USE_SOFT == 0)
#endif
//------------------------------------------------------------------------------
// Pulse Meter Energy monitor
// Enable support by passing PULSEMETER_SUPPORT=1 build flag
@ -950,33 +992,35 @@
#define PZEM004T_SUPPORT 0
#endif
#ifndef PZEM004T_USE_SOFT
#define PZEM004T_USE_SOFT 0 // By default, use Hardware serial with GPIO15 (TX) and GPIO13 (RX)
#ifndef PZEM004T_PORT
#define PZEM004T_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
#ifndef PZEM004T_RX_PIN
#define PZEM004T_RX_PIN 13 // Serial RX GPIO (if PZEM004T_USE_SOFT == 1, creates a SoftwareSerial object)
#ifndef PZEM004T_READ_INTERVAL
#define PZEM004T_READ_INTERVAL 1000 // (ms) Minimum interval between device readings. When there are more than one device, interval will be shared
// between devices and each reading will happen after 'interval value' multiplied by the number of devices
#endif
#ifndef PZEM004T_TX_PIN
#define PZEM004T_TX_PIN 15 // Serial TX GPIO (if PZEM004T_USE_SOFT == 1, creates a SoftwareSerial object)
#ifndef PZEM004T_DEVICES_MAX
#define PZEM004T_DEVICES_MAX 4 // Maximum number of active devices
#endif
#ifndef PZEM004T_HW_PORT
#define PZEM004T_HW_PORT Serial // Hardware serial port (if PZEM004T_USE_SOFT == 0)
// ESP8266: Serial1 does not allow receiving data, no point in changing this setting
#ifndef PZEM004T_ADDRESS_1
#define PZEM004T_ADDRESS_1 "192.168.1.1" // Device address, represented as an IPv4 string
// Only the first address is enabled by default. To have more devices, fill in other addresses here, in the custom.h or through settings (pzemAddr#)
#endif
#ifndef PZEM004T_ADDRESSES
#define PZEM004T_ADDRESSES "192.168.1.1" // Device(s) address(es), separated by space, "192.168.1.1 192.168.1.2 192.168.1.3"
#ifndef PZEM004T_ADDRESS_2
#define PZEM004T_ADDRESS_2 "" // Only one device enabled by default
#endif
#ifndef PZEM004T_READ_INTERVAL
#define PZEM004T_READ_INTERVAL 1500 // Read interval between same device
#ifndef PZEM004T_ADDRESS_3
#define PZEM004T_ADDRESS_3 "" // Only one device enabled by default
#endif
#ifndef PZEM004T_MAX_DEVICES
#define PZEM004T_MAX_DEVICES 3
#ifndef PZEM004T_ADDRESS_4
#define PZEM004T_ADDRESS_4 "" // Only one device enabled by default
#endif
//------------------------------------------------------------------------------
@ -992,17 +1036,9 @@
#define PZEM004TV30_ADDRESS 0xF8 // Default: factory value
#endif
#ifndef PZEM004TV30_USE_SOFT
#define PZEM004TV30_USE_SOFT 0 // By default, use Hardware serial with GPIO15 (TX) and GPIO13 (RX)
// (but, make sure that DEBUG_SERIAL_SUPPORT is set to 0)
#endif
#ifndef PZEM004TV30_RX_PIN
#define PZEM004TV30_RX_PIN 13 // Serial RX GPIO (if PZEM004T_USE_SOFT == 1, creates a SoftwareSerial object)
#endif
#ifndef PZEM004TV30_TX_PIN
#define PZEM004TV30_TX_PIN 15 // Serial TX GPIO (if PZEM004T_USE_SOFT == 1, creates a SoftwareSerial object)
#ifndef PZEM004TV30_PORT
#define PZEM004TV30_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
#ifndef PZEM004TV30_DEBUG
@ -1018,12 +1054,9 @@
#define SDS011_SUPPORT 0
#endif
#ifndef SDS011_RX_PIN
#define SDS011_RX_PIN 14
#endif
#ifndef SDS011_TX_PIN
#define SDS011_TX_PIN 12
#ifndef SDS011_PORT
#define SDS011_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
//------------------------------------------------------------------------------
@ -1035,12 +1068,9 @@
#define SENSEAIR_SUPPORT 0
#endif
#ifndef SENSEAIR_RX_PIN
#define SENSEAIR_RX_PIN 0
#endif
#ifndef SENSEAIR_TX_PIN
#define SENSEAIR_TX_PIN 2
#ifndef SENSEAIR_PORT
#define SENSEAIR_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
//------------------------------------------------------------------------------
@ -1078,12 +1108,9 @@
#define SM300D2_SUPPORT 0
#endif
#ifndef SM300D2_RX_PIN
#define SM300D2_RX_PIN 13
#endif
#ifndef SM300D2_BAUDRATE
#define SM300D2_BAUDRATE 9600
#ifndef SM300D2_PORT
#define SM300D2_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
//------------------------------------------------------------------------------
@ -1133,12 +1160,9 @@
#define T6613_SUPPORT 0
#endif
#ifndef T6613_RX_PIN
#define T6613_RX_PIN 4
#endif
#ifndef T6613_TX_PIN
#define T6613_TX_PIN 5
#ifndef T6613_PORT
#define T6613_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
//------------------------------------------------------------------------------
@ -1163,22 +1187,26 @@
#define V9261F_SUPPORT 0
#endif
#ifndef V9261F_PIN
#define V9261F_PIN 2 // TX pin from the V9261F
#ifndef V9261F_PORT
#define V9261F_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 4800` and `UART[1-3]_INVERT 1`)
#endif
#ifndef V9261F_PIN_INVERSE
#define V9261F_PIN_INVERSE 1 // Signal is inverted
#ifndef V9261F_SYNC_INTERVAL
#define V9261F_SYNC_INTERVAL 600 // Sync signal length (ms)
#endif
#define V9261F_SYNC_INTERVAL 600 // Sync signal length (ms)
#define V9261F_BAUDRATE 4800 // UART baudrate
#ifndef V9261F_POWER_ACTIVE_FACTOR
#define V9261F_POWER_ACTIVE_FACTOR 153699.0
#endif
// Default ratios
#define V9261F_CURRENT_FACTOR 79371434.0
#ifndef V9261F_VOLTAGE_FACTOR
#define V9261F_VOLTAGE_FACTOR 4160651.0
#define V9261F_POWER_FACTOR 153699.0
#define V9261F_RPOWER_FACTOR V9261F_CURRENT_FACTOR
#endif
#ifndef V9261F_CURRENT_FACTOR
#define V9261F_CURRENT_FACTOR 79371434.0
#endif
//------------------------------------------------------------------------------
// VEML6075 based power sensor
@ -1260,12 +1288,9 @@
#define EZOPH_SUPPORT 0
#endif
#ifndef EZOPH_RX_PIN
#define EZOPH_RX_PIN 13 // Software serial RX GPIO
#endif
#ifndef EZOPH_TX_PIN
#define EZOPH_TX_PIN 15 // Software serial TX GPIO
#ifndef EZOPH_PORT
#define EZOPH_PORT 1 // By default, use the first port
// (needs `UART[1-3]_BAUDRATE 9600`)
#endif
#ifndef EZOPH_SYNC_INTERVAL
@ -1285,6 +1310,14 @@
#define ADE7953_ADDRESS 0x38
#endif
#ifndef ADE7953_LINE_CYCLES
#define ADE7953_LINE_CYCLES 50.0f
#endif
#ifndef ADE7953_CURRENT_THRESHOLD
#define ADE7953_CURRENT_THRESHOLD 2000
#endif
// -----------------------------------------------------------------------------
// SI1145 UV Sensor over I2C
// Enable support by passing SI1145_SUPPORT=1 build flag
@ -1321,6 +1354,47 @@
#endif // non-volatile memory. A common value would be every 6h or
// 360 * 60 * 1000 milliseconds. By default, this is disabled.
// -----------------------------------------------------------------------------
// INA219
// Enable support by passing INA219_SUPPORT=1 build flag
// -----------------------------------------------------------------------------
#ifndef INA219_SUPPORT
#define INA219_SUPPORT 0
#endif
#ifndef INA219_ADDRESS
#define INA219_ADDRESS 0x00 // 0x00 means auto
#endif
#ifndef INA219_OPERATING_MODE
#define INA219_OPERATING_MODE SHUNT_AND_BUS_CONTINUOUS
#endif
#ifndef INA219_SHUNT_MODE
#define INA219_SHUNT_MODE BIT_MODE_12
#endif
#ifndef INA219_SHUNT_RESISTANCE
#define INA219_SHUNT_RESISTANCE 0.1 // Ohms
#endif
#ifndef INA219_BUS_MODE
#define INA219_BUS_MODE BIT_MODE_12
#endif
#ifndef INA219_BUS_RANGE
#define INA219_BUS_RANGE BRNG_32
#endif
#ifndef INA219_GAIN
#define INA219_GAIN PG_320
#endif
#ifndef INA219_MAX_EXPECTED_CURRENT
#define INA219_MAX_EXPECTED_CURRENT 2.0 // A
#endif
// -----------------------------------------------------------------------------
// ADC
// -----------------------------------------------------------------------------
@ -1368,15 +1442,36 @@
// Configuration helpers
// =============================================================================
// UART support for sensors using serial port
// (notice that baudrate and mode config is set *externally*)
#if (\
CSE7766_SUPPORT || \
MHZ19_SUPPORT || \
PM1006_SUPPORT || \
PMSX003_SUPPORT || \
PZEM004T_SUPPORT || \
SENSEAIR_SUPPORT || \
SDS011_SUPPORT || \
SM300D2_SUPPORT || \
T6613_SUPPORT || \
V9261F_SUPPORT || \
EZOPH_SUPPORT || \
PZEM004TV30_SUPPORT \
)
#undef UART_SUPPORT
#define UART_SUPPORT 1
#endif
// I2C support when sensor needs it
#if ( ADE7953_SUPPORT || \
AM2320_SUPPORT || \
BH1750_SUPPORT || \
BME680_SUPPORT || \
BMP180_SUPPORT || \
BMX280_SUPPORT || \
BME680_SUPPORT || \
EMON_ADC121_SUPPORT || \
EMON_ADS1X15_SUPPORT || \
INA219_SUPPORT || \
SHT3X_I2C_SUPPORT || \
SI1145_SUPPORT || \
SI7021_SUPPORT || \
@ -1409,13 +1504,14 @@
AM2320_SUPPORT || \
ANALOG_SUPPORT || \
BH1750_SUPPORT || \
BMP180_SUPPORT || \
BME680_SUPPORT || \
BMP180_SUPPORT || \
BMX280_SUPPORT || \
CSE7766_SUPPORT || \
DALLAS_SUPPORT || \
DHT_SUPPORT || \
DIGITAL_SUPPORT || \
DUMMY_SENSOR_SUPPORT || \
ECH1560_SUPPORT || \
EMON_ADC121_SUPPORT || \
EMON_ADS1X15_SUPPORT || \
@ -1424,15 +1520,19 @@
EZOPH_SUPPORT || \
GEIGER_SUPPORT || \
GUVAS12SD_SUPPORT || \
HDC1080_SUPPORT || \
HLW8012_SUPPORT || \
INA219_SUPPORT || \
LDR_SUPPORT || \
MAX6675_SUPPORT || \
MHZ19_SUPPORT || \
MICS2710_SUPPORT || \
MICS5525_SUPPORT || \
NTC_SUPPORT || \
PM1006_SUPPORT || \
PMSX003_SUPPORT || \
PULSEMETER_SUPPORT || \
PZEM004TV30_SUPPORT || \
PZEM004T_SUPPORT || \
SDS011_SUPPORT || \
SENSEAIR_SUPPORT || \
@ -1446,9 +1546,7 @@
TMP3X_SUPPORT || \
V9261F_SUPPORT || \
VEML6075_SUPPORT || \
VL53L1X_SUPPORT || \
HDC1080_SUPPORT || \
PZEM004TV30_SUPPORT \
VL53L1X_SUPPORT \
)
#endif

+ 80
- 56
code/espurna/config/types.h View File

@ -9,8 +9,6 @@
// GPIO
// -----------------------------------------------------------------------------
#define GPIO_NONE 0x99
#define GPIO_TYPE_NONE GpioType::None
#define GPIO_TYPE_HARDWARE GpioType::Hardware
#define GPIO_TYPE_MCP23S08 GpioType::Mcp23s08
@ -19,25 +17,24 @@
// BUTTONS
//------------------------------------------------------------------------------
#define BUTTON_ACTION_NONE ButtonAction::None
#define BUTTON_ACTION_TOGGLE ButtonAction::Toggle
#define BUTTON_ACTION_ON ButtonAction::On
#define BUTTON_ACTION_OFF ButtonAction::Off
#define BUTTON_ACTION_AP ButtonAction::AccessPoint
#define BUTTON_ACTION_RESET ButtonAction::Reset
#define BUTTON_ACTION_PULSE ButtonAction::Pulse
#define BUTTON_ACTION_FACTORY ButtonAction::FactoryReset
#define BUTTON_ACTION_WPS ButtonAction::Wps
#define BUTTON_ACTION_SMART_CONFIG ButtonAction::SmartConfig
#define BUTTON_ACTION_DIM_UP ButtonAction::BrightnessIncrease
#define BUTTON_ACTION_DIM_DOWN ButtonAction::BrightnessDecrease
#define BUTTON_ACTION_DISPLAY_ON ButtonAction::DisplayOn
#define BUTTON_ACTION_CUSTOM ButtonAction::Custom
#define BUTTON_ACTION_FAN_LOW ButtonAction::FanLow
#define BUTTON_ACTION_FAN_MEDIUM ButtonAction::FanMedium
#define BUTTON_ACTION_FAN_HIGH ButtonAction::FanHigh
#define BUTTON_ACTION_MAX ButtonsActionMax
#define BUTTON_ACTION_NONE ButtonAction::None
#define BUTTON_ACTION_TOGGLE ButtonAction::Toggle
#define BUTTON_ACTION_ON ButtonAction::On
#define BUTTON_ACTION_OFF ButtonAction::Off
#define BUTTON_ACTION_AP ButtonAction::AccessPoint
#define BUTTON_ACTION_RESET ButtonAction::Reset
#define BUTTON_ACTION_PULSE ButtonAction::Pulse
#define BUTTON_ACTION_FACTORY ButtonAction::FactoryReset
#define BUTTON_ACTION_WPS ButtonAction::Wps
#define BUTTON_ACTION_SMART_CONFIG ButtonAction::SmartConfig
#define BUTTON_ACTION_DIM_UP ButtonAction::BrightnessIncrease
#define BUTTON_ACTION_DIM_DOWN ButtonAction::BrightnessDecrease
#define BUTTON_ACTION_DISPLAY_ON ButtonAction::DisplayOn
#define BUTTON_ACTION_CUSTOM ButtonAction::Custom
#define BUTTON_ACTION_FAN_LOW ButtonAction::FanLow
#define BUTTON_ACTION_FAN_MEDIUM ButtonAction::FanMedium
#define BUTTON_ACTION_FAN_HIGH ButtonAction::FanHigh
#define BUTTON_ACTION_TERMINAL_COMMAND ButtonAction::TerminalCommand
// Deprecated: legacy mapping, changed to action from above
#define BUTTON_MODE_NONE BUTTON_ACTION_NONE
@ -65,8 +62,10 @@
// configure where do we get the button events
#define BUTTON_PROVIDER_NONE ButtonProvider::None
#define BUTTON_PROVIDER_DUMMY ButtonProvider::Dummy
#define BUTTON_PROVIDER_GPIO ButtonProvider::Gpio
#define BUTTON_PROVIDER_ANALOG ButtonProvider::Analog
#define BUTTON_PROVIDER_LIGHTFOX ButtonProvider::Lightfox
//------------------------------------------------------------------------------
// ENCODER
@ -81,33 +80,37 @@
#define RELAY_NONE 0x99
#define RELAY_BOOT_OFF 0
#define RELAY_BOOT_ON 1
#define RELAY_BOOT_SAME 2
#define RELAY_BOOT_TOGGLE 3
#define RELAY_BOOT_LOCKED_OFF 4
#define RELAY_BOOT_LOCKED_ON 5
#define RELAY_BOOT_OFF RelayBoot::Off
#define RELAY_BOOT_ON RelayBoot::On
#define RELAY_BOOT_SAME RelayBoot::Same
#define RELAY_BOOT_TOGGLE RelayBoot::Toggle
#define RELAY_BOOT_LOCKED_OFF RelayBoot::LockedOff
#define RELAY_BOOT_LOCKED_ON RelayBoot::LockedOn
#define RELAY_TYPE_NORMAL RelayType::Normal
#define RELAY_TYPE_INVERSE RelayType::Inverse
#define RELAY_TYPE_LATCHED RelayType::Latched
#define RELAY_TYPE_LATCHED_INVERSE RelayType::LatchedInverse
#define RELAY_SYNC_ANY 0
#define RELAY_SYNC_NONE_OR_ONE 1
#define RELAY_SYNC_ONE 2
#define RELAY_SYNC_SAME 3
#define RELAY_SYNC_FIRST 4
#define RELAY_SYNC_ANY RelaySync::None
#define RELAY_SYNC_NONE_OR_ONE RelaySync::ZeroOrOne
#define RELAY_SYNC_ONE RelaySync::JustOne
#define RELAY_SYNC_SAME RelaySync::All
#define RELAY_SYNC_FIRST RelaySync::First
#define RELAY_PULSE_NONE RelayPulse::None
#define RELAY_PULSE_OFF RelayPulse::Off
#define RELAY_PULSE_ON RelayPulse::On
#define RELAY_PULSE_NONE Mode::None
#define RELAY_PULSE_OFF Mode::Off
#define RELAY_PULSE_ON Mode::On
#define RELAY_PROVIDER_NONE RelayProvider::None
#define RELAY_PROVIDER_DUMMY RelayProvider::Dummy
#define RELAY_PROVIDER_GPIO RelayProvider::Gpio
#define RELAY_PROVIDER_DUAL RelayProvider::Dual
#define RELAY_PROVIDER_STM RelayProvider::Stm
#define RELAY_PROVIDER_LIGHT_STATE RelayProvider::LightState
#define RELAY_PROVIDER_FAN RelayProvider::Fan
#define RELAY_PROVIDER_LIGHTFOX RelayProvider::Lightfox
#define RELAY_PROVIDER_TUYA RelayProvider::Tuya
#define RFB_PROVIDER_RCSWITCH 0
#define RFB_PROVIDER_EFM8BB1 1
@ -166,9 +169,9 @@
// Heartbeat
//------------------------------------------------------------------------------
#define HEARTBEAT_NONE heartbeat::Mode::None
#define HEARTBEAT_ONCE heartbeat::Mode::Once
#define HEARTBEAT_REPEAT heartbeat::Mode::Repeat
#define HEARTBEAT_NONE espurna::heartbeat::Mode::None
#define HEARTBEAT_ONCE espurna::heartbeat::Mode::Once
#define HEARTBEAT_REPEAT espurna::heartbeat::Mode::Repeat
//------------------------------------------------------------------------------
// MQTT
@ -191,14 +194,14 @@
#define LED_MODE_MANUAL LedMode::Manual // LED will be managed manually (OFF by default)
#define LED_MODE_WIFI LedMode::WiFi // LED will blink according to the WIFI status
#define LED_MODE_FOLLOW LedMode::Follow // LED will follow state of linked LED#_RELAY relay ID
#define LED_MODE_FOLLOW_INVERSE LedMode::FollowInverse // LED will follow the opposite state of linked LED#_RELAY relay ID
#define LED_MODE_FOLLOW LedMode::Relay // LED will follow state of linked LED#_RELAY relay ID
#define LED_MODE_FOLLOW_INVERSE LedMode::RelayInverse // LED will follow the opposite state of linked LED#_RELAY relay ID
#define LED_MODE_FINDME LedMode::FindMe // LED will be ON if all relays are OFF
#define LED_MODE_FINDME_WIFI LedMode::FindMeWiFi // A mixture between WIFI and FINDME
#define LED_MODE_ON LedMode::On // LED always ON
#define LED_MODE_OFF LedMode::Off // LED always OFF
#define LED_MODE_RELAY LedMode::Relay // If any relay is ON, LED will be ON, otherwise OFF
#define LED_MODE_RELAY_WIFI LedMode::RelayWiFi // A mixture between WIFI and RELAY, the reverse of MIXED
#define LED_MODE_RELAY LedMode::Relays // If any relay is ON, LED will be ON, otherwise OFF
#define LED_MODE_RELAY_WIFI LedMode::RelaysWiFi // A mixture between WIFI and RELAY, the reverse of MIXED
// -----------------------------------------------------------------------------
// UI
@ -225,10 +228,10 @@
// SCHEDULER
// -----------------------------------------------------------------------------
#define SCHEDULER_TYPE_NONE 0
#define SCHEDULER_TYPE_SWITCH 1
#define SCHEDULER_TYPE_DIM 2
#define SCHEDULER_TYPE_CURTAIN 3
#define SCHEDULER_TYPE_NONE scheduler::Type::None
#define SCHEDULER_TYPE_RELAY scheduler::Type::Relay
#define SCHEDULER_TYPE_CHANNEL scheduler::Type::Channel
#define SCHEDULER_TYPE_CURTAIN scheduler::Type::Curtain
// -----------------------------------------------------------------------------
// IR
@ -256,6 +259,14 @@
#define LIGHT_EFFECT_FADE 3
#define LIGHT_EFFECT_SMOOTH 4
//------------------------------------------------------------------------------
// PWM
//------------------------------------------------------------------------------
#define PWM_PROVIDER_NONE 0
#define PWM_PROVIDER_GENERIC 1
#define PWM_PROVIDER_ARDUINO 2
//------------------------------------------------------------------------------
// ENVIRONMENTAL
//------------------------------------------------------------------------------
@ -328,6 +339,8 @@
#define SENSOR_PZEM004TV30_ID 41
#define SENSOR_BME680_ID 42
#define SENSOR_SM300D2_ID 43
#define SENSOR_PM1006_ID 44
#define SENSOR_INA219_ID 45
//--------------------------------------------------------------------------------
// Magnitudes
@ -349,8 +362,8 @@
#define MAGNITUDE_ANALOG 12
#define MAGNITUDE_DIGITAL 13
#define MAGNITUDE_EVENT 14
#define MAGNITUDE_PM1dot0 15
#define MAGNITUDE_PM2dot5 16
#define MAGNITUDE_PM1DOT0 15
#define MAGNITUDE_PM2DOT5 16
#define MAGNITUDE_PM10 17
#define MAGNITUDE_CO2 18
#define MAGNITUDE_LUX 19
@ -376,6 +389,9 @@
#define MAGNITUDE_MAX 39
// TODO: backwards compatible sensor integer values. should probably allow custom messsages
// (even with the increased flash arequirements)
#define SENSOR_ERROR_OK 0 // No error
#define SENSOR_ERROR_OUT_OF_RANGE 1 // Result out of sensor range
#define SENSOR_ERROR_WARM_UP 2 // Sensor is warming-up
@ -385,16 +401,13 @@
#define SENSOR_ERROR_I2C 6 // Wrong or locked I2C address
#define SENSOR_ERROR_GPIO_USED 7 // The GPIO is already in use
#define SENSOR_ERROR_CALIBRATION 8 // Calibration error or Not calibrated
#define SENSOR_ERROR_OVERFLOW 9 // Value overflow
#define SENSOR_ERROR_NOT_READY 10 // Device is not ready / available / disconnected
#define SENSOR_ERROR_CONFIG 11 // Configuration values were invalid
#define SENSOR_ERROR_SUPPORT 12 // Not supported
#define SENSOR_ERROR_OTHER 99 // Any other error
#define SENSOR_ERROR_MAX 9
//------------------------------------------------------------------------------
// Telnet server
//------------------------------------------------------------------------------
#define TELNET_SERVER_ASYNC 0
#define TELNET_SERVER_WIFISERVER 1
#define SENSOR_ERROR_MAX 13
//------------------------------------------------------------------------------
// OTA Client (not related to the Web OTA support)
@ -415,3 +428,14 @@
#define SECURE_CLIENT_CHECK_NONE 0 // !!! INSECURE CONNECTION !!!
#define SECURE_CLIENT_CHECK_FINGERPRINT 1 // legacy fingerprint validation
#define SECURE_CLIENT_CHECK_CA 2 // set trust anchor from PROGMEM CA certificate
//------------------------------------------------------------------------------
// WiFi
//------------------------------------------------------------------------------
#define WIFI_SLEEP_MODE_NONE NONE_SLEEP_T
#define WIFI_SLEEP_MODE_MODEM MODEM_SLEEP_T
#define WIFI_SLEEP_MODE_LIGHT LIGHT_SLEEP_T
#define WIFI_DISABLED BootMode::Disabled
#define WIFI_ENABLED BootMode::Enabled

+ 1
- 1
code/espurna/config/version.h View File

@ -21,5 +21,5 @@
#endif
#ifndef CFG_VERSION
#define CFG_VERSION 8
#define CFG_VERSION 14
#endif

+ 182
- 89
code/espurna/crash.cpp View File

@ -13,109 +13,106 @@ Copyright (C) 2019-2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com
// https://github.com/krzychb/EspSaveCrash
// -----------------------------------------------------------------------------
#include "crash.h"
#include "espurna.h"
#if DEBUG_SUPPORT
#include <stdio.h>
#include <stdarg.h>
#include "crash.h"
#include "system.h"
#include "rtcmem.h"
#include "storage_eeprom.h"
constexpr uint32_t EmptyTimestamp { 0xffffffff };
bool _save_crash_enabled = true;
size_t crashReservedSize() {
if (!_save_crash_enabled) return 0;
return CrashReservedSize;
}
#include <cstdio>
#include <cstdarg>
/**
* Save crash information in EEPROM
* This function is called automatically if ESP8266 suffers an exception
* It should be kept quick / consise to be able to execute before hardware wdt may kick in
* This method assumes EEPROM has already been initialized, which is the first thing ESPurna does
* Structure of the single crash data set
*
* 1. Crash time
* 2. Restart reason
* 3. Exception cause
* 4. epc1
* 5. epc2
* 6. epc3
* 7. excvaddr
* 8. depc
* 9. adress of stack start
* 10. adress of stack end
* 11. stack trace size
* 12. stack trace bytes
* ...
*/
extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end ) {
#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes
#define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte
#define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte
#define SAVE_CRASH_EPC1 0x06 // 4 bytes
#define SAVE_CRASH_EPC2 0x0A // 4 bytes
#define SAVE_CRASH_EPC3 0x0E // 4 bytes
#define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes
#define SAVE_CRASH_DEPC 0x16 // 4 bytes
#define SAVE_CRASH_STACK_START 0x1A // 4 bytes
#define SAVE_CRASH_STACK_END 0x1E // 4 bytes
#define SAVE_CRASH_STACK_SIZE 0x22 // 2 bytes
#define SAVE_CRASH_STACK_TRACE 0x24 // variable, 4 bytes per value
static constexpr int EepromCrashBegin = EepromReservedSize;
static constexpr int EepromCrashEnd = 256;
static constexpr size_t CrashReservedSize = EepromCrashEnd - EepromCrashBegin;
static constexpr size_t CrashTraceReservedSize = CrashReservedSize - SAVE_CRASH_STACK_TRACE;
static constexpr uint32_t EmptyTimestamp { 0xffffffff };
namespace debug {
namespace {
namespace crash {
namespace internal {
// Small safeguard to protect from calling crash handler very early on boot.
if (!eepromReady()) {
return;
}
bool enabled = true;
// If we crash more than once in a row, don't store (similar) crash log every time
if (systemStabilityCounter() > 1) {
return;
}
} // namespace internal
// Do not record crash data when doing a normal reboot or when crash trace was disabled
if (checkNeedsReset()) {
return;
}
namespace build {
if (!_save_crash_enabled) {
return;
}
constexpr bool enabled() {
return 1 == SAVE_CRASH_ENABLED;
}
// We will use this later as a marker that there was a crash
uint32_t crash_time = millis();
eepromPut(EepromCrashBegin + SAVE_CRASH_CRASH_TIME, crash_time);
} // namespace build
// XXX rst_info::reason and ::exccause are uint32_t, but are holding small values
// make sure we are using ::write() instead of ::put(), former tries to deduce the required size based on variable type
eepromWrite(EepromCrashBegin + SAVE_CRASH_RESTART_REASON,
static_cast<uint8_t>(rst_info->reason));
eepromWrite(EepromCrashBegin + SAVE_CRASH_EXCEPTION_CAUSE,
static_cast<uint8_t>(rst_info->exccause));
namespace settings {
// write epc1, epc2, epc3, excvaddr and depc to EEPROM as uint32_t
eepromPut(EepromCrashBegin + SAVE_CRASH_EPC1, rst_info->epc1);
eepromPut(EepromCrashBegin + SAVE_CRASH_EPC2, rst_info->epc2);
eepromPut(EepromCrashBegin + SAVE_CRASH_EPC3, rst_info->epc3);
eepromPut(EepromCrashBegin + SAVE_CRASH_EXCVADDR, rst_info->excvaddr);
eepromPut(EepromCrashBegin + SAVE_CRASH_DEPC, rst_info->depc);
bool enabled() {
return getSetting("sysCrashSave", build::enabled());
}
// EEPROM size is limited, write as little as possible.
// we definitely want to avoid big stack traces, e.g. like when stack_end == 0x3fffffb0 and we are in SYS context.
// but still should get enough relevant info and it is possible to set needed size at build/runtime
const uint16_t stack_size = constrain((stack_end - stack_start), 0, CrashReservedSize);
eepromPut(EepromCrashBegin + SAVE_CRASH_STACK_START, stack_start);
eepromPut(EepromCrashBegin + SAVE_CRASH_STACK_END, stack_end);
eepromPut(EepromCrashBegin + SAVE_CRASH_STACK_SIZE, stack_size);
} // namespace settings
// write stack trace to EEPROM and avoid overwriting settings and reserved data
// [EEPROM RESERVED SPACE] >>> ... CRASH DATA ... >>> [SETTINGS]
int eeprom_addr = EepromCrashBegin + SAVE_CRASH_STACK_TRACE;
bool enabled() {
return internal::enabled;
}
auto *addr = reinterpret_cast<uint32_t*>(stack_start);
while (EepromCrashEnd > eeprom_addr) {
eepromPut(eeprom_addr, *addr);
eeprom_addr += sizeof(uint32_t);
++addr;
void enableFromSettings() {
internal::enabled = settings::enabled();
}
size_t reserved() {
if (enabled()) {
return CrashReservedSize;
}
eepromForceCommit();
return 0;
}
/**
* Clears crash info CRASH_TIME value, later checked in crashDump()
*/
void crashClear() {
// Simply reset the timestamp to stop dump() from printing the output more than once per crash.
void clear() {
eepromPut(EepromCrashBegin + SAVE_CRASH_CRASH_TIME, EmptyTimestamp);
eepromCommit();
}
namespace {
/**
* Print out crash information that has been previusly saved in EEPROM
* We can optionally check for the recorded crash time before proceeding.
*/
void _crashDump(Print& print, bool check) {
// Print out crash information that has been previusly saved in EEPROM
// Optionally, check whether the timestamp is erased / EEPROM contains no data.
void dump(Print& print, bool check) {
char buffer[256] = {0};
uint32_t crash_time;
@ -174,15 +171,19 @@ void _crashDump(Print& print, bool check) {
eepromGet(EepromCrashBegin + SAVE_CRASH_STACK_END, stack_end);
eepromGet(EepromCrashBegin + SAVE_CRASH_STACK_SIZE, stack_size);
if ((0 == stack_size) || (0xffff == stack_size)) return;
stack_size = constrain(stack_size, 0, CrashTraceReservedSize);
if ((0 == stack_size) || (0xffff == stack_size)) {
return;
}
static constexpr uint16_t StackMin { 0 };
static constexpr uint16_t StackMax { CrashTraceReservedSize };
stack_size = std::clamp(stack_size, StackMin, StackMax);
// offset is technically an unknown, Core's crash handler only gives us `stack_start` as `sp_dump + offset`
// (...maybe we can hack Core / walk the stack / etc... but, that's not really portable between versions)
snprintf_P(buffer, sizeof(buffer),
PSTR("\n>>>stack>>>\n\nctx: todo\nsp: %08x end: %08x offset: 0000\n"),
stack_start, stack_end
);
stack_start, stack_end);
print.print(buffer);
constexpr auto step = sizeof(uint32_t);
@ -199,7 +200,7 @@ void _crashDump(Print& print, bool check) {
snprintf_P(buffer, sizeof(buffer),
PSTR("%08x: %08x %08x %08x %08x \n"),
stack_start + offset,
stack_start + (offset * 4),
addr1, addr2, addr3, addr4
);
print.print(buffer);
@ -208,32 +209,116 @@ void _crashDump(Print& print, bool check) {
offset += step;
}
snprintf_P(buffer, sizeof(buffer), PSTR("<<<stack<<<\n"));
static const char Tail[] PROGMEM = "<<<stack<<<\n";
memcpy_P(buffer, Tail, sizeof(Tail));
print.print(buffer);
}
void forceDump(Print& print) {
dump(print, false);
}
void dump(Print& print) {
dump(print, true);
}
#if TERMINAL_SUPPORT
PROGMEM_STRING(Name, "CRASH");
void _crashTerminalCommand(const terminal::CommandContext& ctx) {
crashForceDump(ctx.output);
void command(::terminal::CommandContext&& ctx) {
debug::crash::forceDump(ctx.output);
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Name, command},
};
#endif
} // namespace crash
} // namespace
} // namespace debug
/**
* Save crash information in EEPROM
* This function is called automatically if ESP8266 suffers an exception
* It should be kept quick / consise to be able to execute before hardware wdt may kick in
* This method assumes EEPROM has already been initialized, which is the first thing ESPurna does
*/
extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end ) {
// Small safeguard to protect from calling crash handler very early on boot.
if (!eepromReady()) {
return;
}
// If we crash more than once in a row, don't store (similar) crash log every time
if (systemStabilityCounter() > 1) {
return;
}
// Do not record crash data when doing a normal reboot or when crash trace was disabled
if (pendingDeferredReset()) {
return;
}
if (!debug::crash::enabled()) {
return;
}
// We will use this later as a marker that there was a crash
uint32_t crash_time = millis();
eepromPut(EepromCrashBegin + SAVE_CRASH_CRASH_TIME, crash_time);
// XXX rst_info::reason and ::exccause are uint32_t, but are holding small values
// make sure we are using ::write() instead of ::put(), former tries to deduce the required size based on variable type
eepromWrite(EepromCrashBegin + SAVE_CRASH_RESTART_REASON,
static_cast<uint8_t>(rst_info->reason));
eepromWrite(EepromCrashBegin + SAVE_CRASH_EXCEPTION_CAUSE,
static_cast<uint8_t>(rst_info->exccause));
// write epc1, epc2, epc3, excvaddr and depc to EEPROM as uint32_t
eepromPut(EepromCrashBegin + SAVE_CRASH_EPC1, rst_info->epc1);
eepromPut(EepromCrashBegin + SAVE_CRASH_EPC2, rst_info->epc2);
eepromPut(EepromCrashBegin + SAVE_CRASH_EPC3, rst_info->epc3);
eepromPut(EepromCrashBegin + SAVE_CRASH_EXCVADDR, rst_info->excvaddr);
eepromPut(EepromCrashBegin + SAVE_CRASH_DEPC, rst_info->depc);
// EEPROM size is limited, write as little as possible.
// we definitely want to avoid big stack traces, e.g. like when stack_end == 0x3fffffb0 and we are in SYS context.
// but still should get enough relevant info and it is possible to set needed size at build/runtime
static constexpr uint32_t StackMin { 0 };
static constexpr uint32_t StackMax { CrashTraceReservedSize };
const uint16_t stack_size = std::clamp((stack_end - stack_start), StackMin, StackMax);
eepromPut(EepromCrashBegin + SAVE_CRASH_STACK_START, stack_start);
eepromPut(EepromCrashBegin + SAVE_CRASH_STACK_END, stack_end);
eepromPut(EepromCrashBegin + SAVE_CRASH_STACK_SIZE, stack_size);
// write stack trace to EEPROM and avoid overwriting settings and reserved data
// [EEPROM RESERVED SPACE] >>> ... CRASH DATA ... >>> [SETTINGS]
int eeprom_addr = EepromCrashBegin + SAVE_CRASH_STACK_TRACE;
auto *addr = reinterpret_cast<uint32_t*>(stack_start);
while (EepromCrashEnd > eeprom_addr) {
eepromPut(eeprom_addr, *addr);
eeprom_addr += sizeof(uint32_t);
++addr;
}
eepromForceCommit();
}
void crashForceDump(Print& print) {
_crashDump(print, false);
debug::crash::forceDump(print);
}
void crashDump(Print& print) {
_crashDump(print, true);
debug::crash::dump(print);
}
void crashResetReason(Print& print) {
auto reason = customResetReason();
const auto reason = customResetReason();
bool custom { CustomResetReason::None != reason };
print.printf_P(PSTR("last reset reason: %s\n"), custom
? customResetReasonToPayload(reason).c_str()
@ -246,16 +331,24 @@ void crashResetReason(Print& print) {
crashDump(print);
}
size_t crashReservedSize() {
return debug::crash::reserved();
}
void crashClear() {
debug::crash::clear();
}
void crashSetup() {
if (!rtcmemStatus()) {
crashClear();
debug::crash::clear();
}
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("CRASH"), _crashTerminalCommand);
espurna::terminal::add(debug::crash::Commands);
#endif
_save_crash_enabled = getSetting("sysCrashSave", 1 == SAVE_CRASH_ENABLED);
debug::crash::enableFromSettings();
}
#endif // DEBUG_SUPPORT

+ 0
- 39
code/espurna/crash.h View File

@ -15,46 +15,7 @@ Copyright (C) 2019-2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#pragma once
#include "espurna.h"
#include <Arduino.h>
#include <cstdint>
/**
* Structure of the single crash data set
*
* 1. Crash time
* 2. Restart reason
* 3. Exception cause
* 4. epc1
* 5. epc2
* 6. epc3
* 7. excvaddr
* 8. depc
* 9. adress of stack start
* 10. adress of stack end
* 11. stack trace size
* 12. stack trace bytes
* ...
*/
#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes
#define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte
#define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte
#define SAVE_CRASH_EPC1 0x06 // 4 bytes
#define SAVE_CRASH_EPC2 0x0A // 4 bytes
#define SAVE_CRASH_EPC3 0x0E // 4 bytes
#define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes
#define SAVE_CRASH_DEPC 0x16 // 4 bytes
#define SAVE_CRASH_STACK_START 0x1A // 4 bytes
#define SAVE_CRASH_STACK_END 0x1E // 4 bytes
#define SAVE_CRASH_STACK_SIZE 0x22 // 2 bytes
#define SAVE_CRASH_STACK_TRACE 0x24 // variable, 4 bytes per value
constexpr int EepromCrashBegin = EepromReservedSize;
constexpr int EepromCrashEnd = 256;
constexpr size_t CrashReservedSize = EepromCrashEnd - EepromCrashBegin;
constexpr size_t CrashTraceReservedSize = CrashReservedSize - SAVE_CRASH_STACK_TRACE;
size_t crashReservedSize();


+ 26
- 32
code/espurna/curtain_kingart.cpp View File

@ -10,27 +10,17 @@ Copyright (C) 2020 - Eric Chauvet
*/
#include "curtain_kingart.h"
#include "espurna.h"
#if KINGART_CURTAIN_SUPPORT
#include "curtain_kingart.h"
#include "mqtt.h"
#include "ntp.h"
#include "ntp_timelib.h"
#include "settings.h"
#include "ws.h"
#ifndef KINGART_CURTAIN_PORT
#define KINGART_CURTAIN_PORT Serial // Hardware serial port by default
#endif
#ifndef KINGART_CURTAIN_BUFFER_SIZE
#define KINGART_CURTAIN_BUFFER_SIZE 100 // Local UART buffer size
#endif
#define KINGART_CURTAIN_TERMINATION '\e' // Termination character after each message
#define KINGART_CURTAIN_BAUDRATE 19200 // Serial speed is fixed for the device
// --> Let see after if we define a curtain generic switch, use these for now
#define CURTAIN_BUTTON_UNKNOWN -1
#define CURTAIN_BUTTON_PAUSE 0
@ -47,6 +37,8 @@ Copyright (C) 2020 - Eric Chauvet
#define KINGART_DEBUG_MSG_P(...) do { if (_curtain_debug_flag) { DEBUG_MSG_P(__VA_ARGS__); } } while(0)
Stream* _curtain_port { nullptr };
char _KACurtainBuffer[KINGART_CURTAIN_BUFFER_SIZE];
bool _KACurtainNewData = false;
@ -116,9 +108,9 @@ void _KASetMoving() {
//------------------------------------------------------------------------------
//Send a buffer to serial
void _KACurtainSend(const char * tx_buffer) {
KINGART_CURTAIN_PORT.print(tx_buffer);
KINGART_CURTAIN_PORT.print(KINGART_CURTAIN_TERMINATION);
KINGART_CURTAIN_PORT.flush();
_curtain_port->print(tx_buffer);
_curtain_port->print(KINGART_CURTAIN_TERMINATION);
_curtain_port->flush();
KINGART_DEBUG_MSG_P(PSTR("[KA] UART OUT %s\n"), tx_buffer);
}
@ -191,8 +183,8 @@ void _KAStopMoving() {
//Receive a buffer from serial
bool _KACurtainReceiveUART() {
static unsigned char ndx = 0;
while (KINGART_CURTAIN_PORT.available() > 0 && !_KACurtainNewData) {
char rc = KINGART_CURTAIN_PORT.read();
while (_curtain_port->available() > 0 && !_KACurtainNewData) {
char rc = _curtain_port->read();
if (rc != KINGART_CURTAIN_TERMINATION) {
_KACurtainBuffer[ndx] = rc;
if (ndx < KINGART_CURTAIN_BUFFER_SIZE - 1) ndx++;
@ -344,7 +336,7 @@ void _KACurtainResult() {
if (buffer.indexOf("enterESPTOUCH") > 0) {
wifiStartAp();
} else if (buffer.indexOf("exitESPTOUCH") > 0) {
deferredReset(100, CustomResetReason::Hardware);
prepareReset(CustomResetReason::Hardware);
} else { //In any other case, update as it could be a move action
curtainUpdateUI();
}
@ -357,21 +349,21 @@ void _KACurtainResult() {
#if MQTT_SUPPORT
//------------------------------------------------------------------------------
void _curtainMQTTCallback(unsigned int type, const char * topic, char * payload) {
void _curtainMQTTCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) {
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribe(MQTT_TOPIC_CURTAIN);
} else if (type == MQTT_MESSAGE_EVENT) {
// Match topic
const String t = mqttMagnitude(const_cast<char*>(topic));
const auto t = mqttMagnitude(topic);
if (t.equals(MQTT_TOPIC_CURTAIN)) {
if (strcmp(payload, "pause") == 0) {
if (payload == "pause") {
_KACurtainSet(CURTAIN_BUTTON_PAUSE);
} else if (strcmp(payload, "on") == 0) {
} else if (payload == "on") {
_KACurtainSet(CURTAIN_BUTTON_OPEN);
} else if (strcmp(payload, "off") == 0) {
} else if (payload == "off") {
_KACurtainSet(CURTAIN_BUTTON_CLOSE);
} else {
_curtain_position_set = String(payload).toInt();
_curtain_position_set = payload.toString().toInt();
_KACurtainSet(CURTAIN_BUTTON_UNKNOWN, _curtain_position_set);
}
}
@ -393,9 +385,8 @@ void _curtainWebSocketOnConnected(JsonObject& root) {
}
//------------------------------------------------------------------------------
bool _curtainWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, "curtain", strlen("curtain")) == 0) return true;
return false;
bool _curtainWebSocketOnKeyCheck(espurna::StringView key, const JsonVariant& value) {
return espurna::settings::query::samePrefix(key, STRING_VIEW("curtain"));
}
//------------------------------------------------------------------------------
@ -431,7 +422,7 @@ void _curtainWebSocketOnAction(uint32_t client_id, const char * action, JsonObje
}
void _curtainWebSocketOnVisible(JsonObject& root) {
root["curtainVisible"] = 1;
wsPayloadModule(root, PSTR("curtain"));
}
#endif //WEB_SUPPORT
@ -468,9 +459,12 @@ void _KACurtainLoop() {
//------------------------------------------------------------------------------
void kingartCurtainSetup() {
const auto port = uartPort(KINGART_CURTAIN_PORT - 1);
if (!port || (!port->tx || !port->rx)) {
return;
}
// Init port to receive and send messages
KINGART_CURTAIN_PORT.begin(KINGART_CURTAIN_BAUDRATE);
_curtain_port = port->stream;
#if MQTT_SUPPORT
// Register MQTT callback only when supported
@ -493,9 +487,9 @@ void kingartCurtainSetup() {
}
//------------------------------------------------------------------------------
void curtainSetPosition(unsigned char id, long value) {
void curtainSetPosition(unsigned char id, int value) {
if (id > 1) return;
_KACurtainSet(CURTAIN_BUTTON_UNKNOWN, constrain(value, 0, 100));
_KACurtainSet(CURTAIN_BUTTON_UNKNOWN, std::clamp(value, 0, 100));
}
unsigned char curtainCount() {


+ 1
- 3
code/espurna/curtain_kingart.h View File

@ -7,8 +7,6 @@ Copyright (C) 2019 by Albert Weterings
*/
#include "espurna.h"
void kingartCurtainSetup();
void curtainSetPosition(unsigned char id, long value);
void curtainSetPosition(unsigned char id, int value);
unsigned char curtainCount();

BIN
code/espurna/data/index.all.html.gz View File


BIN
code/espurna/data/index.curtain.html.gz View File


BIN
code/espurna/data/index.garland.html.gz View File


BIN
code/espurna/data/index.light.html.gz View File


BIN
code/espurna/data/index.lightfox.html.gz View File


BIN
code/espurna/data/index.rfbridge.html.gz View File


BIN
code/espurna/data/index.rfm69.html.gz View File


BIN
code/espurna/data/index.sensor.html.gz View File


BIN
code/espurna/data/index.small.html.gz View File


BIN
code/espurna/data/index.thermostat.html.gz View File


+ 585
- 300
code/espurna/debug.cpp
File diff suppressed because it is too large
View File


+ 2
- 6
code/espurna/debug.h View File

@ -6,11 +6,7 @@ DEBUG MODULE
#pragma once
#include <Arduino.h>
extern "C" {
void custom_crash_callback(struct rst_info*, uint32_t, uint32_t);
}
#include <cstdint>
class PrintRaw;
class PrintHex;
@ -26,10 +22,10 @@ bool debugLogBuffer();
void debugWebSetup();
void debugConfigure();
void debugConfigureBoot();
void debugShowBanner();
void debugSetup();
void debugSendRaw(const char* line, bool timestamp = false);
void debugSendBytes(const uint8_t* bytes, size_t size);
void debugSend(const char* format, ...);
void debugSend_P(const char* format, ...);

+ 419
- 180
code/espurna/domoticz.cpp View File

@ -3,13 +3,15 @@
DOMOTICZ MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#include "domoticz.h"
#include "espurna.h"
#if DOMOTICZ_SUPPORT
#include "domoticz.h"
#include "light.h"
#include "mqtt.h"
#include "relay.h"
@ -17,190 +19,378 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "sensor.h"
#include "ws.h"
bool _dcz_enabled = false;
std::bitset<RelaysMax> _dcz_relay_state;
#include <ArduinoJson.h>
#include <bitset>
//------------------------------------------------------------------------------
// Private methods
//------------------------------------------------------------------------------
namespace espurna {
namespace domoticz {
namespace {
unsigned int _domoticzIdx(unsigned char relayID, unsigned int defaultValue = 0) {
return getSetting({"dczRelayIdx", relayID}, defaultValue);
}
struct Idx {
constexpr static size_t Default { 0 };
int _domoticzRelay(unsigned int idx) {
for (unsigned char relayID=0; relayID<relayCount(); relayID++) {
if (_domoticzIdx(relayID) == idx) {
return relayID;
}
constexpr Idx() = default;
constexpr explicit Idx(size_t value) :
_value(value)
{}
constexpr explicit operator bool() const {
return _value != Default;
}
return -1;
}
void _domoticzMqttSubscribe(bool value) {
constexpr bool operator==(size_t other) const {
return _value == other;
}
const String dczTopicOut = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC);
if (value) {
mqttSubscribeRaw(dczTopicOut.c_str());
} else {
mqttUnsubscribeRaw(dczTopicOut.c_str());
constexpr bool operator==(const Idx& other) {
return _value == other._value;
}
constexpr size_t value() const {
return _value;
}
private:
size_t _value { Default };
};
} // namespace
} // namespace domoticz
namespace settings {
namespace internal {
template <>
espurna::domoticz::Idx convert(const String& value) {
return espurna::domoticz::Idx(convert<size_t>(value));
}
bool _domoticzStatus(unsigned char id) {
return _dcz_relay_state[id];
} // namespace internal
} // namespace settings
namespace domoticz {
namespace internal {
namespace {
bool enabled { false };
} // namespace
} // namespace internal
namespace {
bool enabled() {
return internal::enabled;
}
void _domoticzStatus(unsigned char id, bool status) {
_dcz_relay_state[id] = status;
relayStatus(id, status);
void enable() {
internal::enabled = true;
}
void disable() {
internal::enabled = false;
}
} // namespace
namespace build {
namespace {
static constexpr ::espurna::domoticz::Idx DefaultIdx;
const __FlashStringHelper* topicOut() {
return F(DOMOTICZ_OUT_TOPIC);
}
const __FlashStringHelper* topicIn() {
return F(DOMOTICZ_IN_TOPIC);
}
constexpr bool enabled() {
return 1 == DOMOTICZ_ENABLED;
}
} // namespace
} // namespace build
namespace settings {
namespace keys {
PROGMEM_STRING(Enabled, "dczEnabled");
PROGMEM_STRING(TopicOut, "dczTopicOut");
PROGMEM_STRING(TopicIn, "dczTopicIn");
#if RELAY_SUPPORT
PROGMEM_STRING(RelayIdx, "dczRelayIdx");
#endif
#if SENSOR_SUPPORT
PROGMEM_STRING(MagnitudeIdx, "dczMagnitude");
#endif
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
PROGMEM_STRING(LightIdx, "dczLightIdx");
#endif
#include "light.h"
} // namespace keys
void _domoticzLight(unsigned int idx, const JsonObject& root) {
namespace {
JsonObject& color = root["Color"];
if (color.success()) {
bool enabled() {
return getSetting(FPSTR(keys::Enabled), build::enabled());
}
// for ColorMode... see:
// https://github.com/domoticz/domoticz/blob/development/hardware/ColorSwitch.h
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Set_a_light_to_a_certain_color_or_color_temperature
String topicOut() {
return getSetting(FPSTR(keys::TopicOut), build::topicOut());
}
DEBUG_MSG_P(PSTR("[DOMOTICZ] Received rgb:%u,%u,%u ww:%u,cw:%u t:%u brightness:%u for IDX %u\n"),
color["r"].as<unsigned char>(),
color["g"].as<unsigned char>(),
color["b"].as<unsigned char>(),
color["ww"].as<unsigned char>(),
color["cw"].as<unsigned char>(),
color["t"].as<unsigned char>(),
color["Level"].as<unsigned char>(),
idx
);
String topicIn() {
return getSetting(FPSTR(keys::TopicIn), build::topicIn());
}
// m field contains information about color mode (enum ColorMode from domoticz ColorSwitch.h):
unsigned int cmode = color["m"];
#if RELAY_SUPPORT
Idx relayIdx(size_t id) {
return getSetting({FPSTR(keys::RelayIdx), id}, build::DefaultIdx);
}
#endif
if (cmode == 2) { // ColorModeWhite - WW,CW,temperature (t unused for now)
#if SENSOR_SUPPORT
Idx magnitudeIdx(size_t id) {
return getSetting({FPSTR(keys::MagnitudeIdx), id}, build::DefaultIdx);
}
#endif
if (lightChannels() < 2) return;
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
Idx lightIdx() {
return getSetting(FPSTR(keys::LightIdx), build::DefaultIdx);
}
#endif
lightChannel(0, color["ww"]);
lightChannel(1, color["cw"]);
} // namespace
} // namespace settings
} else if (cmode == 3 || cmode == 4) { // ColorModeRGB or ColorModeCustom
#if RELAY_SUPPORT
namespace relay {
namespace internal {
namespace {
if (lightChannels() < 3) return;
std::bitset<RelaysMax> status;
lightChannel(0, color["r"]);
lightChannel(1, color["g"]);
lightChannel(2, color["b"]);
} // namespace
} // namespace internal
// WARM WHITE (or MONOCHROME WHITE) and COLD WHITE are always sent.
// Apply only when supported.
if (lightChannels() > 3) {
lightChannel(3, color["ww"]);
}
if (lightChannels() > 4) {
lightChannel(4, color["cw"]);
}
namespace {
void send(Idx, bool);
void send();
size_t find(Idx idx) {
const auto Relays = relayCount();
for (size_t id = 0; id < Relays; ++id) {
if (idx == settings::relayIdx(id)) {
return id;
}
}
// domoticz uses 100 as maximum value while we're using a custom scale
lightBrightness((root["Level"].as<long>() / 100l) * Light::BrightnessMax);
lightUpdate();
return RelaysMax;
}
void status(Idx idx, bool value) {
auto id = find(idx);
if (id < RelaysMax) {
internal::status[id] = value;
::relayStatus(id, value);
}
}
void callback(size_t id, bool value) {
if (internal::status[id] != value) {
internal::status[id] = value;
send(settings::relayIdx(id), value);
}
}
void setup() {
::relayOnStatusChange(callback);
}
} // namespace
} // namespace relay
#endif
void _domoticzMqtt(unsigned int type, const char * topic, char * payload) {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
namespace light {
namespace {
if (!_dcz_enabled) return;
void status(const JsonObject& root, unsigned char nvalue) {
JsonObject& color = root[F("Color")];
if (color.success()) {
const String dczTopicOut = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC);
// for ColorMode... see:
// https://github.com/domoticz/domoticz/blob/development/hardware/ColorSwitch.h
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Set_a_light_to_a_certain_color_or_color_temperature
if (type == MQTT_CONNECT_EVENT) {
auto r = color["r"].as<long>();
auto g = color["g"].as<long>();
auto b = color["b"].as<long>();
// Subscribe to domoticz action topics
mqttSubscribeRaw(dczTopicOut.c_str());
auto cw = color["cw"].as<long>();
auto ww = color["ww"].as<long>();
// Send relays state on connection
#if RELAY_SUPPORT
domoticzSendRelays();
#endif
DEBUG_MSG_P(PSTR("[DOMOTICZ] Dimmer nvalue:%hhu rgb:%ld,%ld,%ld ww:%ld,cw:%ld t:%ld brightness:%ld\n"),
nvalue, r, g, b, ww, cw, color["t"].as<long>(), color[F("Level")].as<long>());
// m field contains information about color mode (enum ColorMode from domoticz ColorSwitch.h):
switch (color["m"].as<int>()) {
// ColorModeWhite - WW,CW,temperature (t unused for now)
case 2:
lightColdWhite(cw);
lightWarmWhite(ww);
break;
// ColorModeRGB or ColorModeCustom
// WARM WHITE (or MONOCHROME WHITE) and COLD WHITE are always in the payload,
// but it only applies when supported by the lights module.
case 3:
case 4:
lightRed(r);
lightGreen(g);
lightBlue(b);
lightColdWhite(cw);
lightWarmWhite(ww);
break;
}
}
if (type == MQTT_MESSAGE_EVENT) {
// domoticz uses 100 as maximum value while we're using a custom scale
lightBrightnessPercent(root[F("Level")].as<long>());
lightState(nvalue > 0);
lightUpdate();
}
} // namespace
} // namespace light
#endif
namespace mqtt {
namespace {
// Check topic
if (dczTopicOut.equals(topic)) {
void subscribe() {
mqttSubscribeRaw(settings::topicOut().c_str());
}
void unsubscribe() {
mqttUnsubscribeRaw(settings::topicOut().c_str());
}
void callback(unsigned int type, espurna::StringView topic, espurna::StringView payload) {
if (!enabled()) {
return;
}
// Parse response
if (type == MQTT_CONNECT_EVENT) {
subscribe();
#if RELAY_SUPPORT
relay::send();
#endif
return;
}
if (type == MQTT_MESSAGE_EVENT) {
const auto out = settings::topicOut();
if (topic == out) {
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.parseObject(payload);
JsonObject& root = jsonBuffer.parseObject(payload.begin());
if (!root.success()) {
DEBUG_MSG_P(PSTR("[DOMOTICZ] Error parsing data\n"));
return;
}
// IDX
unsigned int idx = root["idx"];
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
String stype = root["stype"];
String switchType = root["switchType"];
if ((_domoticzIdx(0) == idx) && (stype.startsWith("RGB") || (switchType.equals("Dimmer")))) {
_domoticzLight(idx, root);
}
#endif
int relayID = _domoticzRelay(idx);
if (relayID >= 0) {
unsigned char value = root["nvalue"];
DEBUG_MSG_P(PSTR("[DOMOTICZ] Received value %u for IDX %u\n"), value, idx);
_domoticzStatus(relayID, value >= 1);
unsigned char nvalue = root[F("nvalue")];
Idx idx(root[F("idx")].as<size_t>());
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
String stype = root[F("stype")];
String switchType = root[F("switchType")];
if ((idx == settings::lightIdx()) && (stype.startsWith(F("RGB")) || (switchType.equals(F("Dimmer"))))) {
espurna::domoticz::light::status(root, nvalue);
return;
}
#endif
#if RELAY_SUPPORT
espurna::domoticz::relay::status(idx, nvalue > 0);
#endif
return;
}
}
}
void send(Idx idx, int nvalue, const char* svalue) {
if (!enabled()) {
return;
}
if (!idx) {
return;
}
StaticJsonBuffer<JSON_OBJECT_SIZE(3)> json;
JsonObject& root = json.createObject();
root[F("idx")] = idx.value();
root[F("nvalue")] = nvalue;
root[F("svalue")] = svalue;
char payload[128] = {0};
root.printTo(payload);
mqttSendRaw(settings::topicIn().c_str(), payload);
}
[[gnu::unused]]
void send(Idx idx, int nvalue) {
send(idx, nvalue, "");
}
void setup() {
::mqttRegister(callback);
}
void _domoticzConfigure() {
const bool enabled = getSetting("dczEnabled", 1 == DOMOTICZ_ENABLED);
if (enabled != _dcz_enabled) _domoticzMqttSubscribe(enabled);
} // namespace
} // namespace mqtt
#if RELAY_SUPPORT
for (size_t id = 0; id < relayCount(); ++id) {
_dcz_relay_state[id] = relayStatus(id);
}
#endif
namespace relay {
namespace {
_dcz_enabled = enabled;
void send(Idx idx, bool value) {
mqtt::send(idx, value ? 1 : 0);
}
void _domoticzRelayCallback(size_t id, bool status) {
if (_domoticzStatus(id) != status) {
_dcz_relay_state[id] = status;
domoticzSendRelay(id, status);
void send() {
const size_t Relays { relayCount() };
for (size_t id = 0; id < Relays; ++id) {
send(settings::relayIdx(id), ::relayStatus(id));
}
}
} // namespace
} // namespace relay
#endif
#if SENSOR_SUPPORT
namespace sensor {
namespace {
void domoticzSendMagnitude(unsigned char type, unsigned char index, double value, const char* buffer) {
if (!_dcz_enabled) return;
void send(unsigned char index, const espurna::sensor::Value& value) {
if (!enabled()) {
return;
}
char key[15];
snprintf_P(key, sizeof(key), PSTR("dczMagnitude%d"), index);
auto idx = settings::magnitudeIdx(index);
if (!idx) {
return;
}
// Domoticz expects some additional data, dashboard might break otherwise.
@ -208,122 +398,171 @@ void domoticzSendMagnitude(unsigned char type, unsigned char index, double value
// TODO: Must send 'forecast' data. Default is last 3 hours:
// https://github.com/domoticz/domoticz/blob/6027b1d9e3b6588a901de42d82f3a6baf1374cd1/hardware/I2C.cpp#L1092-L1193
// For now, just send invalid value. Consider simplifying sampling function and adding it here, with custom sampling time (3 hours, 6 hours, 12 hours etc.)
if (MAGNITUDE_PRESSURE == type) {
String svalue = buffer;
svalue += ";-1";
domoticzSend(key, 0, svalue.c_str());
if (MAGNITUDE_PRESSURE == value.type) {
mqtt::send(idx, 0, (value.repr + F(";-1")).c_str());
// Special case to allow us to use it with switches directly
} else if (MAGNITUDE_DIGITAL == type) {
int nvalue = (buffer[0] >= 48) ? (buffer[0] - 48) : 0;
domoticzSend(key, nvalue, buffer);
} else if (MAGNITUDE_DIGITAL == value.type) {
mqtt::send(idx, (*value.repr.c_str() == '1') ? 1 : 0, value.repr.c_str());
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Humidity
// nvalue contains HUM (relative humidity)
// svalue contains HUM_STAT, one of consts below
} else if (MAGNITUDE_HUMIDITY == type) {
} else if (MAGNITUDE_HUMIDITY == value.type) {
const char status = 48 + (
(value > 70) ? HUMIDITY_WET :
(value > 45) ? HUMIDITY_COMFORTABLE :
(value > 30) ? HUMIDITY_NORMAL :
(value.value > 70) ? HUMIDITY_WET :
(value.value > 45) ? HUMIDITY_COMFORTABLE :
(value.value > 30) ? HUMIDITY_NORMAL :
HUMIDITY_DRY
);
char svalue[2] = {status, '\0'};
domoticzSend(key, static_cast<int>(value), svalue);
const char svalue[2] = {status, '\0'};
mqtt::send(idx, static_cast<int>(value.value), svalue);
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Air_quality
// nvalue contains the ppm
// svalue is not used (?)
} else if (MAGNITUDE_CO2 == type) {
domoticzSend(key, static_cast<int>(value), "");
// Otherwise, send char string (nvalue is only for integers)
} else if (MAGNITUDE_CO2 == value.type) {
mqtt::send(idx, static_cast<int>(value.value));
// Otherwise, send char string aka formatted float (nvalue is only for integers)
} else {
domoticzSend(key, 0, buffer);
mqtt::send(idx, 0, value.repr.c_str());
}
}
} // namespace
} // namespace sensor
#endif // SENSOR_SUPPORT
#if WEB_SUPPORT
namespace web {
namespace {
bool _domoticzWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "dcz", 3) == 0);
PROGMEM_STRING(Prefix, "dcz");
bool onKeyCheck(espurna::StringView key, const JsonVariant&) {
return espurna::settings::query::samePrefix(key, Prefix);
}
void _domoticzWebSocketOnVisible(JsonObject& root) {
root["dczVisible"] = static_cast<unsigned char>(haveRelaysOrSensors());
void onVisible(JsonObject& root) {
bool module { false };
#if RELAY_SUPPORT
module = module || (relayCount() > 0);
#endif
#if SENSOR_SUPPORT
module = module || (magnitudeCount() > 0);
#endif
if (module) {
wsPayloadModule(root, Prefix);
}
}
void _domoticzWebSocketOnConnected(JsonObject& root) {
void onConnected(JsonObject& root) {
root[FPSTR(settings::keys::Enabled)] = settings::enabled();
root[FPSTR(settings::keys::TopicIn)] = settings::topicIn();
root[FPSTR(settings::keys::TopicOut)] = settings::topicOut();
root["dczEnabled"] = getSetting("dczEnabled", 1 == DOMOTICZ_ENABLED);
root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC);
root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC);
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
root[FPSTR(settings::keys::LightIdx)] = settings::lightIdx().value();
#endif
JsonArray& relays = root.createNestedArray("dczRelays");
for (unsigned char i=0; i<relayCount(); i++) {
relays.add(_domoticzIdx(i));
#if RELAY_SUPPORT
const size_t Relays { relayCount() };
JsonArray& relays = root.createNestedArray(F("dczRelays"));
for (size_t id = 0; id < Relays; ++id) {
relays.add(settings::relayIdx(id).value());
}
#endif
#if SENSOR_SUPPORT
sensorWebSocketMagnitudes(root, "dcz");
#endif
#if SENSOR_SUPPORT
sensorWebSocketMagnitudes(root, PSTR("dcz"), [](JsonArray& out, size_t index) {
out.add(settings::magnitudeIdx(index).value());
});
#endif
}
void setup() {
wsRegister()
.onVisible(onVisible)
.onConnected(onConnected)
.onKeyCheck(onKeyCheck);
}
} // namespace
} // namespace web
#endif // WEB_SUPPORT
//------------------------------------------------------------------------------
// Public API
//------------------------------------------------------------------------------
template<typename T> void domoticzSend(const char * key, T nvalue, const char * svalue) {
if (!_dcz_enabled) return;
const auto idx = getSetting(key, 0);
if (idx > 0) {
char payload[128];
snprintf(payload, sizeof(payload), "{\"idx\": %d, \"nvalue\": %s, \"svalue\": \"%s\"}", idx, String(nvalue).c_str(), svalue);
mqttSendRaw(getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC).c_str(), payload);
namespace {
void configure() {
auto enabled_in_cfg = settings::enabled();
if (enabled_in_cfg != enabled()) {
if (enabled_in_cfg) {
mqtt::subscribe();
} else {
mqtt::unsubscribe();
}
}
}
template<typename T> void domoticzSend(const char * key, T nvalue) {
domoticzSend(key, nvalue, "");
}
#if RELAY_SUPPORT
for (size_t id = 0; id < relayCount(); ++id) {
relay::internal::status[id] = relayStatus(id);
}
#endif
void domoticzSendRelay(unsigned char relayID, bool status) {
if (!_dcz_enabled) return;
char buffer[15];
snprintf_P(buffer, sizeof(buffer), PSTR("dczRelayIdx%u"), relayID);
domoticzSend(buffer, status ? "1" : "0");
if (enabled_in_cfg) {
enable();
} else {
disable();
}
}
void domoticzSendRelays() {
for (uint8_t relayID=0; relayID < relayCount(); relayID++) {
domoticzSendRelay(relayID, relayStatus(relayID));
#if RELAY_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
void migrate(int version) {
if (version < 10) {
if (relayCount() != 1) {
return;
}
moveSetting(F("dczRelayIdx0"), FPSTR(settings::keys::LightIdx));
}
}
#endif
void domoticzSetup() {
void setup() {
#if RELAY_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
migrateVersion(migrate);
#endif
#if WEB_SUPPORT
web::setup();
#endif
_domoticzConfigure();
#if RELAY_SUPPORT
relay::setup();
#endif
#if WEB_SUPPORT
wsRegister()
.onVisible(_domoticzWebSocketOnVisible)
.onConnected(_domoticzWebSocketOnConnected)
.onKeyCheck(_domoticzWebSocketOnKeyCheck);
#endif
mqtt::setup();
#if RELAY_SUPPORT
relaySetStatusChange(_domoticzRelayCallback);
#endif
::espurnaRegisterReload(configure);
configure();
}
// Callbacks
mqttRegister(_domoticzMqtt);
espurnaRegisterReload(_domoticzConfigure);
} // namespace
} // namespace domoticz
} // namespace espurna
#if SENSOR_SUPPORT
void domoticzSendMagnitude(unsigned char index, const espurna::sensor::Value& value) {
espurna::domoticz::sensor::send(index, value);
}
#endif
bool domoticzEnabled() {
return _dcz_enabled;
return espurna::domoticz::enabled();
}
void domoticzSetup() {
espurna::domoticz::setup();
}
#endif

+ 3
- 17
code/espurna/domoticz.h View File

@ -3,28 +3,14 @@
DOMOTICZ MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#pragma once
#include "espurna.h"
#if DOMOTICZ_SUPPORT
#include <ArduinoJson.h>
#include <bitset>
template<typename T>
void domoticzSend(const char * key, T value);
template<typename T>
void domoticzSend(const char * key, T nvalue, const char * svalue);
void domoticzSendMagnitude(unsigned char type, unsigned char index, double value, const char* buffer);
void domoticzSendRelay(unsigned char relayID, bool status);
void domoticzSendRelays();
#include "sensor.h"
void domoticzSendMagnitude(unsigned char, const espurna::sensor::Value&);
void domoticzSetup();
bool domoticzEnabled();
#endif // DOMOTICZ_SUPPORT == 1

+ 0
- 4
code/espurna/dummy_ets_printf.c View File

@ -1,4 +0,0 @@
// special dummy printf to disable Serial using some boards
int dummy_ets_printf(const char* format, ...) {
return 0;
}

+ 3
- 1
code/espurna/encoder.cpp View File

@ -6,11 +6,13 @@ Copyright (C) 2018-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "encoder.h"
#include "espurna.h"
#if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
#include "encoder.h"
#include "light.h"
#include "libs/Encoder.h"
#include <vector>


+ 0
- 2
code/espurna/encoder.h View File

@ -6,6 +6,4 @@ Copyright (C) 2018-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "espurna.h"
void encoderSetup();

code/espurna/pwm.c → code/espurna/esp8266_pwm.c View File

@ -1,4 +1,5 @@
/*
* https://github.com/StefanBruens/ESP8266_new_pwm
* Copyright (C) 2016 Stefan Brüns <stefan.bruens@rwth-aachen.de>
*
* This program is free software; you can redistribute it and/or modify
@ -16,6 +17,9 @@
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
// Should include our own prototypes, since the original code does not provide them
#include "libs/esp8266_pwm.h"
/* Set the following three defines to your needs */
#ifndef SDK_PWM_PERIOD_COMPAT_MODE
@ -47,7 +51,6 @@
#include <c_types.h>
#include <eagle_soc.h>
#include <ets_sys.h>
#include "libs/pwm.h"
// from SDK hw_timer.c
#define TIMER1_DIVIDE_BY_16 0x0004
@ -66,7 +69,8 @@ struct pwm_phase {
* pwm_next_set from the interrupt routine during the first
* pwm phase
*/
typedef struct pwm_phase (pwm_phase_array)[PWM_MAX_CHANNELS + 2];
#define PWM_PHASES_MAX (PWM_MAX_CHANNELS + 2)
typedef struct pwm_phase (pwm_phase_array)[PWM_PHASES_MAX];
static pwm_phase_array pwm_phases[3];
static struct {
struct pwm_phase* next_set;
@ -74,14 +78,11 @@ static struct {
uint8_t current_phase;
} pwm_state;
static uint32_t pwm_period;
static uint32_t pwm_period_ticks;
static uint32_t pwm_duty[PWM_MAX_CHANNELS];
static uint16_t gpio_mask[PWM_MAX_CHANNELS];
static uint8_t pwm_channels;
// 3-tuples of MUX_REGISTER, MUX_VALUE and GPIO number
typedef uint32_t (pin_info_type)[3];
static uint32_t pwm_period = 0;
static uint32_t pwm_period_ticks = 0;
static uint32_t pwm_duty[PWM_MAX_CHANNELS] = {0};
static uint16_t gpio_mask[PWM_MAX_CHANNELS] = {0};
static uint8_t pwm_channels = {0};
struct gpio_regs {
uint32_t out; /* 0x60000300 */
@ -114,23 +115,27 @@ static struct timer_regs* timer = (struct timer_regs*)(0x60000600);
static void IRAM_ATTR
pwm_intr_handler(void)
{
if ((pwm_state.current_set[pwm_state.current_phase].off_mask == 0) &&
(pwm_state.current_set[pwm_state.current_phase].on_mask == 0)) {
const struct pwm_phase current_phase = pwm_state.current_set[pwm_state.current_phase];
if ((current_phase.off_mask == 0) && (current_phase.on_mask == 0)) {
pwm_state.current_set = pwm_state.next_set;
pwm_state.current_phase = 0;
}
do {
// force write to GPIO registers on each loop
// need barrier to force write to GPIO registers on each loop
// (plus, unlike with volatile variables, memw only added once)
__asm__ volatile ("" : : : "memory");
gpio->out_w1ts = (uint32_t)(pwm_state.current_set[pwm_state.current_phase].on_mask);
gpio->out_w1tc = (uint32_t)(pwm_state.current_set[pwm_state.current_phase].off_mask);
const struct pwm_phase current_phase = pwm_state.current_set[pwm_state.current_phase];
gpio->out_w1ts = (uint32_t)(current_phase.on_mask);
gpio->out_w1tc = (uint32_t)(current_phase.off_mask);
uint32_t ticks = pwm_state.current_set[pwm_state.current_phase].ticks;
uint32_t ticks = current_phase.ticks;
pwm_state.current_phase++;
const uint8_t next_phase = ++pwm_state.current_phase;
pwm_state.current_phase = (next_phase < PWM_PHASES_MAX) ? next_phase : 0;
// TODO busy loop numbers need to depend on cpu speed
if (ticks) {
if (ticks >= 16) {
// constant interrupt overhead
@ -159,7 +164,7 @@ pwm_intr_handler(void)
*/
void ICACHE_FLASH_ATTR
pwm_init(uint32_t period, uint32_t *duty, uint32_t pwm_channel_num,
uint32_t (*pin_info_list)[3])
struct pwm_pin_info *pin_info_list)
{
int i, j, n;
@ -180,10 +185,10 @@ pwm_init(uint32_t period, uint32_t *duty, uint32_t pwm_channel_num,
uint32_t all = 0;
// PIN info: MUX-Register, Mux-Setting, PIN-Nr
for (n = 0; n < pwm_channels; n++) {
pin_info_type* pin_info = &pin_info_list[n];
PIN_FUNC_SELECT((*pin_info)[0], (*pin_info)[1]);
gpio_mask[n] = 1 << (*pin_info)[2];
all |= 1 << (*pin_info)[2];
struct pwm_pin_info* pin_info = &pin_info_list[n];
PIN_FUNC_SELECT(pin_info->addr, pin_info->func);
gpio_mask[n] = 1 << pin_info->pin;
all |= 1 << pin_info->pin;
if (duty)
pwm_set_duty(duty[n], n);
}

+ 14
- 7
code/espurna/espurna.h View File

@ -26,14 +26,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "compat.h"
#include "board.h"
#include "build.h"
#include "types.h"
#include "debug.h"
#include "gpio.h"
#include "storage_eeprom.h"
#include "settings.h"
#include "system.h"
#include "terminal.h"
#include "uart.h"
#include "utils.h"
#include "network.h"
#include "wifi.h"
#include <functional>
@ -44,7 +47,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#if DEBUG_SUPPORT
#define DEBUG_MSG(...) debugSend(__VA_ARGS__)
#define DEBUG_MSG_P(...) debugSend_P(__VA_ARGS__)
#define DEBUG_MSG_P(...) debugSend(__VA_ARGS__)
#endif
#ifndef DEBUG_MSG
@ -52,13 +55,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define DEBUG_MSG_P(...)
#endif
using ReloadCallback = void (*)();
void espurnaRegisterReload(ReloadCallback);
void espurnaReload();
using LoopCallback = void (*)();
void espurnaRegisterLoop(LoopCallback);
void espurnaRegisterLoop(LoopCallback callback);
void espurnaRegisterReload(LoopCallback callback);
void espurnaReload();
void espurnaRegisterOnce(espurna::Callback);
void espurnaRegisterOnceUnique(espurna::Callback::Type);
unsigned long espurnaLoopDelay();
void espurnaLoopDelay(unsigned long);
espurna::duration::Milliseconds espurnaLoopDelay();
void espurnaLoopDelay(espurna::duration::Milliseconds);
void extraSetup();

+ 9
- 0
code/espurna/fan.h View File

@ -8,6 +8,9 @@ Copyright (C) 2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#pragma once
#include <cstddef>
#include <memory>
enum class FanSpeed {
Off,
Low,
@ -15,6 +18,12 @@ enum class FanSpeed {
High
};
class RelayProviderBase;
std::unique_ptr<RelayProviderBase> fanMakeRelayProvider(size_t);
bool fanStatus();
void fanStatus(bool);
void fanSpeed(FanSpeed);
FanSpeed fanSpeed();


+ 18
- 15
code/espurna/filters/BaseFilter.h View File

@ -3,27 +3,30 @@
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT
#pragma once
class BaseFilter {
#include <cstddef>
public:
class BaseFilter {
public:
virtual ~BaseFilter() = default;
virtual ~BaseFilter() {}
// Reset internal value to default and also erases internal storage
virtual void reset() {
}
virtual void add(double value) = 0;
virtual unsigned char count() = 0;
virtual void reset() = 0;
virtual double result() = 0;
virtual void resize(unsigned char size) = 0;
unsigned char size() { return _size; };
// Defaults to 0 aka no backing storage
virtual size_t capacity() const {
return 0;
}
protected:
// Resize the backing storage (when it is available)
virtual void resize(size_t) {
}
unsigned char _size;
// Store reading
virtual void update(double value) = 0;
// Return filtered value
virtual double value() const = 0;
};
#endif // SENSOR_SUPPORT

+ 16
- 29
code/espurna/filters/LastFilter.h View File

@ -3,38 +3,25 @@
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT
#pragma once
#include "BaseFilter.h"
class LastFilter : public BaseFilter {
public:
void add(double value) {
_value = value;
}
unsigned char count() {
return 1;
}
void reset() {
_value = 0;
}
double result() {
return _value;
}
void resize(unsigned char size) {}
protected:
double _value = 0;
public:
void update(double value) override {
_value = value;
}
void reset() override {
_value = 0;
}
double value() const override {
return _value;
}
private:
double _value = 0;
bool _status = false;
};
#endif // SENSOR_SUPPORT

+ 14
- 26
code/espurna/filters/MaxFilter.h View File

@ -3,38 +3,26 @@
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT
#pragma once
#include "BaseFilter.h"
class MaxFilter : public BaseFilter {
public:
void add(double value) {
if (value > _max) _max = value;
}
unsigned char count() {
return 1;
}
#include <algorithm>
void reset() {
_max = 0;
}
double result() {
return _max;
}
void resize(unsigned char size) {}
class MaxFilter : public BaseFilter {
public:
void update(double value) override {
_value = std::max(value, _value);
}
protected:
void reset() override {
_value = 0;
}
double _max = 0;
double value() const {
return _value;
}
private:
double _value = 0;
};
#endif // SENSOR_SUPPORT

+ 56
- 69
code/espurna/filters/MedianFilter.h View File

@ -3,90 +3,77 @@
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT
#pragma once
#include "BaseFilter.h"
class MedianFilter : public BaseFilter {
public:
#include <algorithm>
#include <vector>
~MedianFilter() {
if (_data) delete _data;
}
void add(double value) {
if (_pointer <= _size) {
_data[_pointer] = value;
_pointer++;
}
}
unsigned char count() {
return _pointer;
class MedianFilter : public BaseFilter {
public:
void update(double value) override {
if (_values.size() < _values.capacity()) {
_values.push_back(value);
}
void reset() {
if (_pointer > 0) {
_data[0] = _data[_pointer-1];
_pointer = 1;
} else {
_pointer = 0;
}
}
void reset() override {
if (_values.size() > 2) {
_values[0] = _values.back();
_values.resize(1);
} else {
_values.clear();
}
double result() {
double sum = 0;
if (_pointer > 2) {
for (unsigned char i = 1; i <= _pointer - 2; i++) {
// For each position,
// we find the median with the previous and next value
// and use that for the sum
double previous = _data[i-1];
double current = _data[i];
double next = _data[i+1];
if (previous > current) std::swap(previous, current);
if (current > next) std::swap(current, next);
if (previous > current) std::swap(previous, current);
sum += current;
}
double value() const override {
double sum { 0.0 };
if (_values.size() > 2) {
auto median = [](double previous, double current, double next) {
if (previous < current) {
if (current < next) {
return current;
} else if (previous < next) {
return next;
} else {
return previous;
}
} else if (previous < next) {
return previous;
} else if (current < next) {
return next;
}
sum /= (_pointer - 2);
} else if (_pointer > 0) {
sum = _data[0];
return current;
};
for (auto prev = _values.begin(); prev != (_values.end() - 2); ++prev) {
sum += median(*prev, *std::next(prev, 1), *std::next(prev, 2));
}
return sum;
sum /= (_values.size() - 2);
} else if (_values.size() > 0) {
sum = _values.front();
}
void resize(unsigned char size) {
if (_size == size) return;
_size = size;
if (_data) delete _data;
_data = new double[_size+1];
for (unsigned char i=0; i<=_size; i++) _data[i] = 0;
_pointer = 0;
}
return sum;
}
protected:
size_t capacity() const override {
return _capacity;
}
unsigned char _pointer = 0;
double * _data = NULL;
void resize(size_t capacity) override {
if (_capacity != capacity) {
_capacity = capacity;
_values.clear();
_values.reserve(_capacity + 1);
}
}
private:
std::vector<double> _values;
size_t _capacity = 0;
};
#endif // SENSOR_SUPPORT

+ 28
- 40
code/espurna/filters/MovingAverageFilter.h View File

@ -3,49 +3,37 @@
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT
#pragma once
#include <vector>
#include "BaseFilter.h"
class MovingAverageFilter : public BaseFilter {
public:
void add(double value) {
_sum = _sum + value - _data[_pointer];
_data[_pointer] = value;
_pointer = (_pointer + 1) % _size;
}
unsigned char count() {
return _pointer;
}
void reset() {}
double result() {
return _sum;
}
void resize(unsigned char size) {
if (_size == size) return;
_size = size;
if (_data) delete _data;
_data = new double[_size];
for (unsigned char i=0; i<_size; i++) _data[i] = 0;
_pointer = 0;
_sum = 0;
}
protected:
unsigned char _pointer = 0;
double _sum = 0;
double * _data = NULL;
#include <vector>
class MovingAverageFilter : public BaseFilter {
public:
void update(double value) override {
_sum = _sum + value - _values[_sample];
_values[_sample] = value;
_sample = (_sample + 1) % _values.capacity();
}
size_t capacity() const override {
return _values.capacity();
}
double value() const override {
return _sum;
}
void resize(size_t size) override {
_sum = 0.0;
_sample = 0;
_values.clear();
_values.resize(size, 0.0);
}
private:
std::vector<double> _values {{0.0}};
size_t _sample = 0;
double _sum = 0;
};
#endif // SENSOR_SUPPORT

+ 15
- 25
code/espurna/filters/SumFilter.h View File

@ -3,38 +3,28 @@
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT
#pragma once
#include "BaseFilter.h"
class SumFilter : public BaseFilter {
public:
void update(double value) override {
_value += value;
}
public:
void add(double value) {
_value += value;
}
unsigned char count() {
return 1;
}
void reset() {
_value = 0.0;
}
size_t capacity() const override {
return 1;
}
double result() {
return _value;
}
void reset() override {
_value = 0.0;
}
void resize(unsigned char size) {}
protected:
double _value = 0;
double value() const override {
return _value;
}
private:
double _value = 0.0;
};
#endif // SENSOR_SUPPORT

+ 167
- 164
code/espurna/garland.cpp View File

@ -12,7 +12,7 @@ Currently animation calculation, brightness calculation/transition and showing m
Debug output shows timings. Overal timing should be not more that 3000 ms.
MQTT control:
Topic: DEVICE_NAME/garland/set
Topic: $root/garland/set
Message: {"command":"string", "enable":"string", "brightness":int, "speed":int, "animation":"string",
"palette":"string"/int, "duration":int}
All parameters are optional.
@ -43,44 +43,51 @@ duration does not set), otherwise it just set scene parameters.
Infinite commands can be interrupted by immediate command or by reset command.
*/
#include "garland.h"
#include "espurna.h"
#if GARLAND_SUPPORT
#include <Adafruit_NeoPixel.h>
#include <array>
#include <list>
#include <memory>
#include <queue>
#include <vector>
#include "garland.h"
#include "mqtt.h"
#include "ws.h"
namespace {
#include "garland/color.h"
#include "garland/palette.h"
#include "garland/scene.h"
#include "mqtt.h"
#include "ws.h"
const char* NAME_GARLAND_ENABLED = "garlandEnabled";
const char* NAME_GARLAND_BRIGHTNESS = "garlandBrightness";
const char* NAME_GARLAND_SPEED = "garlandSpeed";
alignas(4) static constexpr char NAME_GARLAND_ENABLED[] = "garlandEnabled";
alignas(4) static constexpr char NAME_GARLAND_BRIGHTNESS[] = "garlandBrightness";
alignas(4) static constexpr char NAME_GARLAND_SPEED[] = "garlandSpeed";
const char* NAME_GARLAND_SWITCH = "garland_switch";
const char* NAME_GARLAND_SET_BRIGHTNESS = "garland_set_brightness";
const char* NAME_GARLAND_SET_SPEED = "garland_set_speed";
const char* NAME_GARLAND_SET_DEFAULT = "garland_set_default";
alignas(4) static constexpr char NAME_GARLAND_SWITCH[] = "garland_switch";
alignas(4) static constexpr char NAME_GARLAND_SET_BRIGHTNESS[] = "garland_set_brightness";
alignas(4) static constexpr char NAME_GARLAND_SET_SPEED[] = "garland_set_speed";
alignas(4) static constexpr char NAME_GARLAND_SET_DEFAULT[] = "garland_set_default";
const char* MQTT_TOPIC_GARLAND = "garland";
alignas(4) static constexpr char MQTT_TOPIC_GARLAND[] = "garland";
const char* MQTT_PAYLOAD_COMMAND = "command";
const char* MQTT_PAYLOAD_ENABLE = "enable";
const char* MQTT_PAYLOAD_BRIGHTNESS = "brightness";
const char* MQTT_PAYLOAD_ANIM_SPEED = "speed";
const char* MQTT_PAYLOAD_ANIMATION = "animation";
const char* MQTT_PAYLOAD_PALETTE = "palette";
const char* MQTT_PAYLOAD_DURATION = "duration";
alignas(4) static constexpr char MQTT_PAYLOAD_COMMAND[] = "command";
alignas(4) static constexpr char MQTT_PAYLOAD_ENABLE[] = "enable";
alignas(4) static constexpr char MQTT_PAYLOAD_BRIGHTNESS[] = "brightness";
alignas(4) static constexpr char MQTT_PAYLOAD_ANIM_SPEED[] = "speed";
alignas(4) static constexpr char MQTT_PAYLOAD_ANIMATION[] = "animation";
alignas(4) static constexpr char MQTT_PAYLOAD_PALETTE[] = "palette";
alignas(4) static constexpr char MQTT_PAYLOAD_DURATION[] = "duration";
const char* MQTT_COMMAND_IMMEDIATE = "immediate";
const char* MQTT_COMMAND_RESET = "reset"; // reset queue
const char* MQTT_COMMAND_QUEUE = "queue"; // enqueue command payload
const char* MQTT_COMMAND_SEQUENCE = "sequence"; // place command to sequence
alignas(4) static constexpr char MQTT_COMMAND_IMMEDIATE[] = "immediate";
alignas(4) static constexpr char MQTT_COMMAND_RESET[] = "reset"; // reset queue
alignas(4) static constexpr char MQTT_COMMAND_QUEUE[] = "queue"; // enqueue command payload
alignas(4) static constexpr char MQTT_COMMAND_SEQUENCE[] = "sequence"; // place command to sequence
#define EFFECT_UPDATE_INTERVAL_MIN 7000 // 5 sec
#define EFFECT_UPDATE_INTERVAL_MAX 12000 // 10 sec
@ -96,7 +103,7 @@ std::queue<String> _command_queue;
std::vector<String> _command_sequence;
// Palette should
Palette pals[] = {
std::array<Palette, 10> pals {
// palettes below are taken from http://www.color-hex.com/color-palettes/ (and modified)
// RGB: Red,Green,Blue sequence
Palette("RGB", {0xFF0000, 0x00FF00, 0x0000FF}),
@ -131,62 +138,52 @@ Palette pals[] = {
Palette("Lime", {0x51f000, 0x6fff00, 0x96ff00, 0xc9ff00, 0xf0ff00}),
// Pastel: Pastel Fruity Mixture
Palette("Pastel", {0x75aa68, 0x5960ae, 0xe4be6c, 0xca5959, 0x8366ac})};
constexpr size_t palsSize() { return sizeof(pals)/sizeof(pals[0]); }
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(GARLAND_LEDS, GARLAND_D_PIN, NEO_GRB + NEO_KHZ800);
Scene scene(&pixels);
Anim* anims[] = {new AnimGlow(), new AnimStart(), new AnimPixieDust(), new AnimSparkr(), new AnimRun(), new AnimStars(), new AnimSpread(),
new AnimRandCyc(), new AnimFly(), new AnimComets(), new AnimAssemble(), new AnimDolphins(), new AnimSalut(), new AnimFountain(), new AnimWaves()};
constexpr size_t animsSize() { return sizeof(anims)/sizeof(anims[0]); }
Palette("Pastel", {0x75aa68, 0x5960ae, 0xe4be6c, 0xca5959, 0x8366ac})
};
constexpr uint16_t GarlandLeds { GARLAND_LEDS };
constexpr unsigned char GarlandPin { GARLAND_DATA_PIN };
constexpr neoPixelType GarlandPixelType { NEO_GRB + NEO_KHZ800 };
Adafruit_NeoPixel pixels(GarlandLeds, GarlandPin, GarlandPixelType);
Scene<GarlandLeds> scene(&pixels);
std::array<Anim*, 15> anims {
new AnimGlow(),
new AnimStart(),
new AnimPixieDust(),
new AnimSparkr(),
new AnimRun(),
new AnimStars(),
new AnimSpread(),
new AnimRandCyc(),
new AnimFly(),
new AnimComets(),
new AnimAssemble(),
new AnimDolphins(),
new AnimSalut(),
new AnimFountain(),
new AnimWaves()
};
#define START_ANIMATION 1
Anim* _currentAnim = anims[1];
Palette* _currentPalette = &pals[0];
auto one_color_palette = std::unique_ptr<Palette>(new Palette("White", {0xffffff}));
//------------------------------------------------------------------------------
void garlandDisable() {
pixels.clear();
}
//------------------------------------------------------------------------------
void garlandEnabled(bool enabled) {
_garland_enabled = enabled;
setSetting(NAME_GARLAND_ENABLED, _garland_enabled);
if (!_garland_enabled) {
schedule_function([]() {
pixels.clear();
pixels.show();
});
}
#if WEB_SUPPORT
char buffer[128];
snprintf_P(buffer, sizeof(buffer), PSTR("{\"garlandEnabled\": %s}"), enabled ? "true" : "false");
wsSend(buffer);
#endif
}
//------------------------------------------------------------------------------
bool garlandEnabled() {
return _garland_enabled;
}
//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
void _garlandConfigure() {
_garland_enabled = getSetting(NAME_GARLAND_ENABLED, true);
DEBUG_MSG_P(PSTR("[GARLAND] _garland_enabled = %d\n"), _garland_enabled);
byte brightness = getSetting(NAME_GARLAND_BRIGHTNESS, 255);
scene.setBrightness(brightness);
DEBUG_MSG_P(PSTR("[GARLAND] brightness = %d\n"), brightness);
float speed = getSetting(NAME_GARLAND_SPEED, 50);
scene.setSpeed(speed);
DEBUG_MSG_P(PSTR("[GARLAND] enabled %s brightness %d speed %s\n"),
_garland_enabled ? "YES" : "NO", brightness, String(speed).c_str());
}
//------------------------------------------------------------------------------
@ -202,27 +199,30 @@ void setDefault() {
byte speed = scene.getSpeed();
setSetting(NAME_GARLAND_SPEED, speed);
#if WEB_SUPPORT
char buffer[128];
snprintf_P(buffer, sizeof(buffer), PSTR("{\"garlandBrightness\": %d, \"garlandSpeed\": %d}"), brightness, speed);
wsSend(buffer);
wsPost([brightness, speed](JsonObject& root) {
root["garlandBrightness"] = brightness;
root["garlandSpeed"] = speed;
});
#endif
}
#if WEB_SUPPORT
//------------------------------------------------------------------------------
void _garlandWebSocketOnVisible(JsonObject& root) {
wsPayloadModule(root, PSTR("garland"));
}
void _garlandWebSocketOnConnected(JsonObject& root) {
root[NAME_GARLAND_ENABLED] = garlandEnabled();
root[NAME_GARLAND_BRIGHTNESS] = scene.getBrightness();
root[NAME_GARLAND_SPEED] = scene.getSpeed();
root["garlandVisible"] = 1;
}
//------------------------------------------------------------------------------
bool _garlandWebSocketOnKeyCheck(const char* key, JsonVariant& value) {
if (strncmp(key, NAME_GARLAND_ENABLED, strlen(NAME_GARLAND_ENABLED)) == 0) return true;
if (strncmp(key, NAME_GARLAND_BRIGHTNESS, strlen(NAME_GARLAND_BRIGHTNESS)) == 0) return true;
if (strncmp(key, NAME_GARLAND_SPEED, strlen(NAME_GARLAND_SPEED)) == 0) return true;
return false;
bool _garlandWebSocketOnKeyCheck(espurna::StringView key, const JsonVariant&) {
return espurna::settings::query::samePrefix(key, NAME_GARLAND_ENABLED)
|| espurna::settings::query::samePrefix(key, NAME_GARLAND_BRIGHTNESS)
|| espurna::settings::query::samePrefix(key, NAME_GARLAND_SPEED);
}
//------------------------------------------------------------------------------
@ -313,7 +313,7 @@ bool executeCommand(const String& command) {
Anim* newAnim = _currentAnim;
if (root.containsKey(MQTT_PAYLOAD_ANIMATION)) {
auto animation = root[MQTT_PAYLOAD_ANIMATION].as<const char*>();
for (size_t i = 0; i < animsSize(); ++i) {
for (size_t i = 0; i < anims.size(); ++i) {
auto anim_name = anims[i]->name();
if (strcmp(animation, anim_name) == 0) {
newAnim = anims[i];
@ -329,11 +329,11 @@ bool executeCommand(const String& command) {
one_color_palette.reset(new Palette("Color", {root[MQTT_PAYLOAD_PALETTE].as<uint32_t>()}));
newPalette = one_color_palette.get();
} else {
auto palette = root[MQTT_PAYLOAD_PALETTE].as<const char*>();
auto palette = root[MQTT_PAYLOAD_PALETTE].as<String>();
bool palette_found = false;
for (size_t i = 0; i < palsSize(); ++i) {
for (size_t i = 0; i < pals.size(); ++i) {
auto pal_name = pals[i].name();
if (strcmp(palette, pal_name) == 0) {
if (palette = pal_name) {
newPalette = &pals[i];
palette_found = true;
scene_setup_required = true;
@ -341,9 +341,9 @@ bool executeCommand(const String& command) {
}
}
if (!palette_found) {
uint32_t color = (uint32_t)strtoul(palette, NULL, 0);
if (color != 0) {
one_color_palette.reset(new Palette("Color", {color}));
const auto result = parseUnsigned(palette);
if (result.ok) {
one_color_palette.reset(new Palette("Color", {result.value}));
newPalette = one_color_palette.get();
}
}
@ -393,12 +393,12 @@ void garlandLoop(void) {
if (!scene_setup_done) {
Anim* newAnim = _currentAnim;
while (newAnim == _currentAnim) {
newAnim = anims[secureRandom(START_ANIMATION + 1, animsSize())];
newAnim = anims[secureRandom(START_ANIMATION + 1, anims.size())];
}
Palette* newPalette = _currentPalette;
while (newPalette == _currentPalette) {
newPalette = &pals[secureRandom(palsSize())];
newPalette = &pals[secureRandom(pals.size())];
}
unsigned long newAnimDuration = secureRandom(EFFECT_UPDATE_INTERVAL_MIN, EFFECT_UPDATE_INTERVAL_MAX);
@ -409,19 +409,16 @@ void garlandLoop(void) {
}
//------------------------------------------------------------------------------
void garlandMqttCallback(unsigned int type, const char * topic, const char * payload) {
void garlandMqttCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) {
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribe(MQTT_TOPIC_GARLAND);
}
if (type == MQTT_MESSAGE_EVENT) {
// Match topic
String t = mqttMagnitude((char*)topic);
auto t = mqttMagnitude(topic);
if (t.equals(MQTT_TOPIC_GARLAND)) {
// Parse JSON input
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(payload);
JsonObject& root = jsonBuffer.parseObject(payload.begin());
if (!root.success()) {
DEBUG_MSG_P(PSTR("[GARLAND] Error parsing mqtt data\n"));
return;
@ -433,7 +430,7 @@ void garlandMqttCallback(unsigned int type, const char * topic, const char * pay
}
if (command == MQTT_COMMAND_IMMEDIATE) {
_immediate_command = payload;
_immediate_command = payload.toString();
} else if (command == MQTT_COMMAND_RESET) {
std::queue<String> empty_queue;
std::swap(_command_queue, empty_queue);
@ -444,38 +441,14 @@ void garlandMqttCallback(unsigned int type, const char * topic, const char * pay
setDefault();
garlandEnabled(true);
} else if (command == MQTT_COMMAND_QUEUE) {
_command_queue.push(payload);
_command_queue.push(payload.toString());
} else if (command == MQTT_COMMAND_SEQUENCE) {
_command_sequence.push_back(payload);
_command_sequence.push_back(payload.toString());
}
}
}
}
//------------------------------------------------------------------------------
void garlandSetup() {
_garlandConfigure();
mqttRegister(garlandMqttCallback);
// Websockets
#if WEB_SUPPORT
wsRegister()
.onConnected(_garlandWebSocketOnConnected)
.onKeyCheck(_garlandWebSocketOnKeyCheck)
.onAction(_garlandWebSocketOnAction);
#endif
espurnaRegisterLoop(garlandLoop);
espurnaRegisterReload(_garlandReload);
pixels.begin();
scene.setAnim(_currentAnim);
scene.setPalette(_currentPalette);
scene.setup();
_currentDuration = secureRandom(EFFECT_UPDATE_INTERVAL_MIN, EFFECT_UPDATE_INTERVAL_MAX);
}
/*#######################################################################
_____
/ ____|
@ -491,52 +464,30 @@ void garlandSetup() {
#define GARLAND_SCENE_DEFAULT_SPEED 50
#define GARLAND_SCENE_DEFAULT_BRIGHTNESS 255
Scene::Scene(Adafruit_NeoPixel* pixels)
: _pixels(pixels),
_numLeds(pixels->numPixels()),
_leds1(_numLeds),
_leds2(_numLeds),
_ledstmp(_numLeds),
_seq(_numLeds) {
}
void Scene::setPalette(Palette* palette) {
template<uint16_t Leds>
void Scene<Leds>::setPalette(Palette* palette) {
_palette = palette;
if (setUpOnPalChange) {
setupImpl();
}
}
void Scene::setBrightness(byte brightness) {
DEBUG_MSG_P(PSTR("[GARLAND] Scene::setBrightness = %d\n"), brightness);
this->brightness = brightness;
}
byte Scene::getBrightness() {
DEBUG_MSG_P(PSTR("[GARLAND] Scene::getBrightness = %d\n"), brightness);
return brightness;
}
// Speed is reverse to cycleFactor and 10x
void Scene::setSpeed(byte speed) {
template<uint16_t Leds>
void Scene<Leds>::setSpeed(byte speed) {
this->speed = speed;
cycleFactor = (float)(GARLAND_SCENE_SPEED_MAX - speed) / GARLAND_SCENE_SPEED_FACTOR;
DEBUG_MSG_P(PSTR("[GARLAND] Scene::setSpeed %d cycleFactor = %d\n"), speed, (int)(cycleFactor * 1000));
}
byte Scene::getSpeed() {
DEBUG_MSG_P(PSTR("[GARLAND] Scene::getSpeed %d cycleFactor = %d\n"), speed, (int)(cycleFactor * 1000));
return speed;
}
void Scene::setDefault() {
template<uint16_t Leds>
void Scene<Leds>::setDefault() {
speed = GARLAND_SCENE_DEFAULT_SPEED;
cycleFactor = (float)(GARLAND_SCENE_SPEED_MAX - speed) / GARLAND_SCENE_SPEED_FACTOR;
brightness = GARLAND_SCENE_DEFAULT_BRIGHTNESS;
DEBUG_MSG_P(PSTR("[GARLAND] Scene::setDefault speed = %d cycleFactor = %d brightness = %d\n"), speed, (int)(cycleFactor * 1000), brightness);
}
void Scene::run() {
template<uint16_t Leds>
void Scene<Leds>::run() {
unsigned long iteration_start_time = micros();
if (state == Calculate || cyclesRemain < 1) {
@ -569,7 +520,7 @@ void Scene::run() {
Color* leds_prev = (_leds == &_leds1[0]) ? &_leds2[0] : &_leds1[0];
if (transc > 0) {
for (int i = 0; i < _numLeds; i++) {
for (int i = 0; i < Leds; i++) {
// transition is in progress
Color c = _leds[i].interpolate(leds_prev[i], transc);
byte r = (int)(bri_lvl[c.r]) * brightness / 256;
@ -578,7 +529,7 @@ void Scene::run() {
_pixels->setPixelColor(i, _pixels->Color(r, g, b));
}
} else {
for (int i = 0; i < _numLeds; i++) {
for (int i = 0; i < Leds; i++) {
// regular operation
byte r = (int)(bri_lvl[_leds[i].r]) * brightness / 256;
byte g = (int)(bri_lvl[_leds[i].g]) * brightness / 256;
@ -601,11 +552,11 @@ void Scene::run() {
Soft WDT reset. To avoid wdt reset we need to switch soft wdt off for long strips.
It is not best practice, but assuming that it is only garland, it can be acceptable.
Tested up to 300 leds. */
if (_numLeds > NUMLEDS_CAN_CAUSE_WDT_RESET) {
if (Leds > NUMLEDS_CAN_CAUSE_WDT_RESET) {
ESP.wdtDisable();
}
_pixels->show();
if (_numLeds > NUMLEDS_CAN_CAUSE_WDT_RESET) {
if (Leds > NUMLEDS_CAN_CAUSE_WDT_RESET) {
ESP.wdtEnable(5000);
}
sum_show_time += (micros() - iteration_start_time);
@ -616,7 +567,8 @@ void Scene::run() {
--cyclesRemain;
}
void Scene::setupImpl() {
template<uint16_t Leds>
void Scene<Leds>::setupImpl() {
transms = millis() + GARLAND_SCENE_TRANSITION_MS;
// switch operation buffers (for transition to operate)
@ -627,11 +579,12 @@ void Scene::setupImpl() {
}
if (_anim) {
_anim->Setup(_palette, _numLeds, _leds, &_ledstmp[0], &_seq[0]);
_anim->Setup(_palette, Leds, _leds, _ledstmp.data(), _seq.data());
}
}
void Scene::setup() {
template<uint16_t Leds>
void Scene<Leds>::setup() {
sum_calc_time = 0;
sum_pixl_time = 0;
sum_show_time = 0;
@ -645,10 +598,6 @@ void Scene::setup() {
}
}
unsigned long Scene::getAvgCalcTime() { return sum_calc_time / calc_num; }
unsigned long Scene::getAvgPixlTime() { return sum_pixl_time / pixl_num; }
unsigned long Scene::getAvgShowTime() { return sum_show_time / show_num; }
/*#######################################################################
_ _ _
/\ (_) | | (_)
@ -666,6 +615,7 @@ void Anim::Setup(Palette* palette, uint16_t numLeds, Color* leds, Color* ledstmp
this->leds = leds;
this->ledstmp = ledstmp;
this->seq = seq;
// TODO: if animation allocates 'stuff', provide some persistent memory locations instead of going to the heap?
SetupImpl();
}
@ -697,14 +647,11 @@ void Anim::glowForEachLed(int i) {
leds[i] = leds[i].brightness(bra);
}
void Anim::glowRun() { braPhase += braPhaseSpd; }
bool operator== (const Color &c1, const Color &c2)
{
return (c1.r == c2.r && c1.g == c2.g && c1.b == c2.b);
void Anim::glowRun() {
braPhase += braPhaseSpd;
}
unsigned int rng() {
unsigned int Anim::rng() {
static unsigned int y = 0;
y += micros(); // seeded with changing number
y ^= y << 2;
@ -713,8 +660,64 @@ unsigned int rng() {
return (y);
}
// Ranom numbers generator in byte range (256) much faster than secureRandom.
// Random numbers generator in byte range (256) much faster than secureRandom.
// For usage in time-critical places.
byte rngb() { return (byte)rng(); }
byte Anim::rngb() {
return static_cast<byte>(rng());
}
} // namespace
//------------------------------------------------------------------------------
void garlandEnabled(bool enabled) {
setSetting(NAME_GARLAND_ENABLED, _garland_enabled);
if (_garland_enabled != enabled) {
espurnaRegisterOnceUnique([]() {
pixels.clear();
pixels.show();
});
}
_garland_enabled = enabled;
#if WEB_SUPPORT
wsPost([enabled](JsonObject& root) {
root["garlandEnabled"] = enabled;
});
#endif
}
bool garlandEnabled() {
return _garland_enabled;
}
void garlandDisable() {
pixels.clear();
}
void garlandSetup() {
_garlandConfigure();
mqttRegister(garlandMqttCallback);
// Websockets
#if WEB_SUPPORT
wsRegister()
.onVisible(_garlandWebSocketOnVisible)
.onConnected(_garlandWebSocketOnConnected)
.onKeyCheck(_garlandWebSocketOnKeyCheck)
.onAction(_garlandWebSocketOnAction);
#endif
espurnaRegisterLoop(garlandLoop);
espurnaRegisterReload(_garlandReload);
pixels.begin();
scene.setAnim(_currentAnim);
scene.setPalette(_currentPalette);
scene.setup();
_currentDuration = secureRandom(EFFECT_UPDATE_INTERVAL_MIN, EFFECT_UPDATE_INTERVAL_MAX);
}
#endif // GARLAND_SUPPORT

+ 3
- 6
code/espurna/garland.h View File

@ -7,10 +7,7 @@ Inspired by https://github.com/Vasil-Pahomov/ArWs2812 (currently https://github.
#pragma once
#include "espurna.h"
#if GARLAND_SUPPORT
void garlandEnabled(bool);
bool garlandEnabled();
void garlandDisable();
void garlandSetup();
#endif

+ 4
- 3
code/espurna/garland/anim.h View File

@ -68,9 +68,10 @@ protected:
//glow animation - must be called at the end of each animaton run
void glowRun();
//random number helpers for animations
static unsigned int rng();
static byte rngb();
private:
const char* _name;
};
unsigned int rng();
byte rngb();

+ 17
- 8
code/espurna/garland/animations/anim_comets.h View File

@ -13,9 +13,15 @@ class AnimComets : public Anim {
}
void SetupImpl() override {
comets.clear();
for (int i = 0; i < 4; ++i)
comets.emplace_back(palette, numLeds);
if (comets.size()) {
for (auto& c : comets) {
c = {palette, numLeds};
}
} else {
for (int i = 0; i < 4; ++i) {
comets.emplace_back(palette, numLeds);
}
}
}
void Run() override {
@ -25,8 +31,7 @@ class AnimComets : public Anim {
int tail = c.head + c.len * -c.dir;
// Check if Comet out of range and generate it again
if ((c.head < 0 && tail < 0) || (c.head >= numLeds && tail >= numLeds)) {
Comet new_comet(palette, numLeds);
std::swap(c, new_comet);
c = {palette, numLeds};
}
for (int l = 0; l < c.len; ++l) {
@ -46,16 +51,20 @@ class AnimComets : public Anim {
float speed = ((float)secureRandom(4, 10)) / 10;
Color color;
int dir = 1;
std::vector<Color> points;
Comet(Palette* pal, uint16_t numLeds) : head(secureRandom(0, numLeds / 2)), color(pal->getRndInterpColor()), points(len) {
std::unique_ptr<Color[]> points;
Comet(Palette* pal, uint16_t numLeds) : head(secureRandom(0, numLeds / 2)), color(pal->getRndInterpColor()) {
// DEBUG_MSG_P(PSTR("[GARLAND] Comet created head = %d len = %d speed = %g cr = %d cg = %d cb = %d\n"), head, len, speed, color.r, color.g, color.b);
if (secureRandom(10) > 5) {
head = numLeds - head;
dir = -1;
}
points.reset(new Color[len]);
for (int i = 0; i < len; ++i) {
points[i] = Color((byte)(color.r * (len - i) / len), (byte)(color.g * (len - i) / len), (byte)(color.b * (len - i) / len));
points[i] = {
(byte)(color.r * (len - i) / len),
(byte)(color.g * (len - i) / len),
(byte)(color.b * (len - i) / len)};
}
}
};


+ 20
- 12
code/espurna/garland/animations/anim_dolphins.h View File

@ -14,9 +14,15 @@ class AnimDolphins : public Anim {
}
void SetupImpl() override {
dolphins.clear();
for (int i = 0; i < 4; ++i)
dolphins.emplace_back(palette, numLeds);
if (dolphins.size()) {
for (auto& d : dolphins) {
d = {palette, numLeds};
}
} else {
for (int i = 0; i < 4; ++i) {
dolphins.emplace_back(palette, numLeds);
}
}
}
void Run() override {
@ -35,7 +41,7 @@ class AnimDolphins : public Anim {
for (int i = 1; i < 5; ++i) {
Dolphin new_dolphin(palette, numLeds);
if (new_dolphin.HaveEnoughSpace(seq)) {
std::swap(d, new_dolphin);
d = std::move(new_dolphin);
break;
}
}
@ -52,8 +58,8 @@ class AnimDolphins : public Anim {
int head = 0;
int start;
Color color;
std::vector<Color> points;
Dolphin(Palette* pal, uint16_t numLeds) : start(secureRandom(0, numLeds - len)), color(pal->getRndInterpColor()), points(len) {
std::unique_ptr<Color[]> points;
Dolphin(Palette* pal, uint16_t numLeds) : start(secureRandom(0, numLeds - len)), color(pal->getRndInterpColor()) {
// DEBUG_MSG_P(PSTR("[GARLAND] Dolphin created start = %d len = %d dir = %d cr = %d cg = %d cb = %d\n"), start, len, dir, color.r, color.g, color.b);
if (secureRandom(10) > 5) {
start = numLeds - start;
@ -61,12 +67,14 @@ class AnimDolphins : public Anim {
}
int halflen = len / 2;
for (int i = 0; i < halflen; ++i) {
points[i] = points[len-i-1] = Color((byte)(color.r * i / halflen), (byte)(color.g * i / halflen), (byte)(color.b * i / halflen));
// DEBUG_MSG_P(PSTR("[GARLAND] Dolphin i=%d cr = %d cg = %d cb = %d\n"), i, points[i].r, points[i].g, points[i].b);
}
if (len > halflen * 2) {
points[halflen] = color;
points.reset(new Color[len]);
for (int i = 0; i < len; ++i) {
int nth = (i > halflen) ? (len - i) : i;
points[i] = {
(byte)(color.r * nth / halflen),
(byte)(color.g * nth / halflen),
(byte)(color.b * nth / halflen)};
}
}


+ 7
- 7
code/espurna/garland/animations/anim_fountain.h View File

@ -35,7 +35,7 @@ class AnimFountain : public Anim {
for (int i = 1; i < 5; ++i) {
Fountain new_fountain(palette, numLeds);
if (new_fountain.HaveEnoughSpace(seq)) {
std::swap(d, new_fountain);
d = std::move(new_fountain);
break;
}
}
@ -52,17 +52,17 @@ class AnimFountain : public Anim {
int head = 0;
int start;
// Color color;
std::vector<Color> points;
Fountain(Palette* pal, uint16_t numLeds) : start(secureRandom(len, numLeds - len)), /*color(pal->getRndInterpColor()),*/ points(len) {
// DEBUG_MSG_P(PSTR("[GARLAND] Fountain created start = %d len = %d dir = %d cr = %d cg = %d cb = %d\n"), start, len, dir, color.r, color.g, color.b);
std::unique_ptr<Color[]> points;
Fountain(Palette* pal, uint16_t numLeds) : start(secureRandom(len, numLeds - len)) /*, color(pal->getRndInterpColor())*/ {
if (secureRandom(10) > 5) {
start = numLeds - start;
start = numLeds - start - 1;
dir = -1;
}
// int halflen = len / 2;
// DEBUG_MSG_P(PSTR("[GARLAND] Fountain created start = %d len = %d dir = %d cr = %d cg = %d cb = %d\n"), start, len, dir, color.r, color.g, color.b);
points.reset(new Color[len]);
for (int i = 0; i < len; ++i) {
points[i] = pal->getRndInterpColor();
points[i] = {pal->getRndInterpColor()};
// DEBUG_MSG_P(PSTR("[GARLAND] Fountain i=%d cr = %d cg = %d cb = %d\n"), i, points[i].r, points[i].g, points[i].b);
}
}


+ 17
- 13
code/espurna/garland/animations/anim_salut.h View File

@ -13,11 +13,15 @@ class AnimSalut : public Anim {
}
void SetupImpl() override {
shots.clear();
// There can be more then one shot at the moment
// but looks like one is enough
// for (int i = 0; i < 3; ++i)
// but looks like one is enough for now
if (shots.size()) {
for (auto& s : shots) {
s = {palette, numLeds};
}
} else {
shots.emplace_back(palette, numLeds);
}
}
void Run() override {
@ -25,8 +29,7 @@ class AnimSalut : public Anim {
for (auto& c : shots) {
if (!c.Run(leds)) {
Shot new_shot(palette, numLeds);
std::swap(c, new_shot);
c = Shot(palette, numLeds);
}
}
}
@ -42,6 +45,7 @@ class AnimSalut : public Anim {
int dir;
Color color;
uint16_t numLeds;
Spark() = default;
Spark(int pos, Palette* pal, uint16_t numLeds) : pos(pos), dir(secureRandom(10) > 5 ? -1 : 1), color(pal->getRndInterpColor()), numLeds(numLeds) {}
void Run(Color* leds) {
if (pos >= 0 && pos < numLeds) {
@ -65,21 +69,21 @@ class AnimSalut : public Anim {
public:
int spark_num = secureRandom(30, 40);
int center;
std::vector<Spark> sparks;
Shot(Palette* pal, uint16_t numLeds) : center(secureRandom(15, numLeds - 15)) {
std::unique_ptr<Spark[]> sparks;
Shot(Palette* pal, uint16_t numLeds) {
// DEBUG_MSG_P(PSTR("[GARLAND] Shot created center = %d spark_num = %d\n"), center, spark_num);
sparks.reserve(spark_num);
int center = secureRandom(15, numLeds - 15);
sparks.reset(new Spark[spark_num]);
for (int i = 0; i < spark_num; ++i) {
sparks.emplace_back(center, pal, numLeds);
sparks[i] = {center, pal, numLeds};
}
}
bool Run(Color* leds) {
bool done = true;
for (auto& s : sparks) {
if (!s.done) {
for (int i = 0; i < spark_num; ++i) {
if (!sparks[i].done) {
done = false;
s.Run(leds);
sparks[i].Run(leds);
}
}
return !done;


+ 0
- 1
code/espurna/garland/animations/anim_sparkr.h View File

@ -23,7 +23,6 @@ class AnimSparkr : public Anim {
void Run() override {
for (int i = 0; i < numLeds; ++i) {
byte pos = seq[i];
leds[pos] = (i > phase) ? prevColor
: (i == phase) ? sparkleColor
: curColor;


+ 18
- 22
code/espurna/garland/color.h View File

@ -7,26 +7,30 @@ Inspired by https://github.com/Vasil-Pahomov/ArWs2812 (currently https://github.
#pragma once
#if GARLAND_SUPPORT
class Color {
public:
byte r = 0;
byte g = 0;
byte b = 0;
#include <Arduino.h>
#include <string>
Color() = default;
Color(const Color&) = default;
Color(Color&&) = default;
struct Color
{
byte r;
byte g;
byte b;
Color& operator=(const Color&) = default;
Color& operator=(Color&&) = default;
inline Color() : r(0), g(0), b(0) {}
bool operator==(const Color& other) const {
return (r == other.r) && (g == other.g) && (b == other.b);
}
// allow construction from R, G, B
inline Color(uint8_t ir, uint8_t ig, uint8_t ib)
Color(uint8_t ir, uint8_t ig, uint8_t ib)
: r(ir), g(ig), b(ib) {
}
// allow construction from 32-bit (really 24-bit) bit 0xRRGGBB color code
inline Color(uint32_t colorcode)
Color(uint32_t colorcode)
: r((colorcode >> 16) & 0xFF), g((colorcode >> 8) & 0xFF), b((colorcode >> 0) & 0xFF) {
}
@ -92,19 +96,11 @@ struct Color
return r == 0 && g == 0 && b == 0;
}
void println() const {
Serial.print(("r="));Serial.print(r);Serial.print((" "));
Serial.print(("g="));Serial.print(g);Serial.print( (" "));
Serial.print(("b="));Serial.println(b);
}
String to_str() {
char buf[20];
sprintf(buf, "r=%hhu, g=%hhu, b=%hhu", r, g, b);
snprintf_P(buf, sizeof(buf),
PSTR("r=%hhu, g=%hhu, b=%hhu"),
r, g, b);
return buf;
}
friend bool operator== (const Color &c1, const Color &c2);
};
#endif // GARLAND_SUPPORT

+ 0
- 6
code/espurna/garland/palette.h View File

@ -7,10 +7,6 @@ Inspired by https://github.com/Vasil-Pahomov/ArWs2812 (currently https://github.
#pragma once
#if GARLAND_SUPPORT
#include <vector>
#include "color.h"
class Palette {
@ -91,5 +87,3 @@ class Palette {
std::vector<Color> _colors;
std::vector<Color> _cache;
};
#endif // GARLAND_SUPPORT

+ 16
- 25
code/espurna/garland/scene.h View File

@ -7,11 +7,6 @@ Inspired by https://github.com/Vasil-Pahomov/ArWs2812 (currently https://github.
#pragma once
#if GARLAND_SUPPORT
#include <array>
#include <vector>
#include "anim.h"
#include "animations/anim_assemble.h"
#include "animations/anim_comets.h"
@ -29,42 +24,40 @@ Inspired by https://github.com/Vasil-Pahomov/ArWs2812 (currently https://github.
#include "animations/anim_start.h"
#include "animations/anim_waves.h"
class Adafruit_NeoPixel;
class Palette;
template <uint16_t Leds>
class Scene {
public:
Scene(Adafruit_NeoPixel* pixels);
Scene(Adafruit_NeoPixel* pixels) : _pixels(pixels) {}
constexpr uint16_t getLeds() const { return Leds; }
void setAnim(Anim* anim) { _anim = anim; }
bool finishedAnimCycle() { return _anim ? _anim->finishedycle() : true; }
unsigned long getAvgCalcTime() { return sum_calc_time / calc_num; }
unsigned long getAvgPixlTime() { return sum_pixl_time / pixl_num; }
unsigned long getAvgShowTime() { return sum_show_time / show_num; }
int getNumShows() { return numShows; }
byte getBrightness() { return brightness; }
void setBrightness(byte value) { brightness = value; }
byte getSpeed() { return speed; }
void setPalette(Palette* palette);
void setBrightness(byte brightness);
byte getBrightness();
void setSpeed(byte speed);
byte getSpeed();
void setDefault();
void setAnim(Anim* anim) { _anim = anim; }
void run();
void setup();
bool finishedAnimCycle() { return _anim ? _anim->finishedycle() : true; }
unsigned long getAvgCalcTime();
unsigned long getAvgPixlTime();
unsigned long getAvgShowTime();
int getNumShows() { return numShows; }
private:
Adafruit_NeoPixel* _pixels = nullptr;
uint16_t _numLeds;
//Color arrays - two for making transition
std::vector<Color> _leds1;
std::vector<Color> _leds2;
std::array<Color, Leds> _leds1;
std::array<Color, Leds> _leds2;
// array of Colorfor anim to currently work with
Color* _leds = nullptr;
Anim* _anim = nullptr;
//auxiliary colors array for mutual usage of anims
std::vector<Color> _ledstmp;
std::vector<byte> _seq;
std::array<Color, Leds> _ledstmp;
std::array<byte, Leds> _seq;
Palette* _palette = nullptr;
@ -119,5 +112,3 @@ private:
void setupImpl();
};
#endif // GARLAND_SUPPORT

+ 868
- 72
code/espurna/gpio.cpp
File diff suppressed because it is too large
View File


+ 71
- 26
code/espurna/gpio.h View File

@ -19,31 +19,50 @@ enum class GpioType : int {
Mcp23s08
};
class GpioBase {
public:
virtual const char* id() const = 0;
virtual size_t pins() const = 0;
virtual bool lock(unsigned char index) const = 0;
virtual void lock(unsigned char index, bool value) = 0;
virtual bool valid(unsigned char index) const = 0;
virtual BasePinPtr pin(unsigned char index) = 0;
namespace espurna {
namespace gpio {
struct Origin {
const char* base;
uint8_t pin;
bool lock;
bool result;
SourceLocation location;
};
struct Mode {
int8_t value;
};
Mode pin_mode(uint8_t);
} // namespace gpio
namespace settings {
namespace internal {
template <>
GpioType convert(const String& value);
String serialize(GpioType);
} // namespace internal
} // namespace settings
} // namespace espurna
class GpioBase {
public:
virtual const char* id() const = 0;
virtual size_t pins() const = 0;
virtual bool lock(unsigned char index) const = 0;
virtual void lock(unsigned char index, bool value) = 0;
virtual bool valid(unsigned char index) const = 0;
virtual BasePinPtr pin(unsigned char index) = 0;
};
GpioBase& hardwareGpio();
GpioBase* gpioBase(GpioType);
BasePinPtr gpioRegister(GpioBase& base, unsigned char gpio);
BasePinPtr gpioRegister(unsigned char gpio);
GpioBase& hardwareGpio();
void hardwareGpioIgnore(unsigned char gpio);
void gpioLockOrigin(espurna::gpio::Origin);
void gpioSetup();
@ -63,30 +82,51 @@ inline bool gpioValid(unsigned char gpio) {
return gpioValid(hardwareGpio(), gpio);
}
inline bool gpioLock(GpioBase& base, unsigned char gpio, bool value) {
if (base.valid(gpio)) {
bool old = base.lock(gpio);
base.lock(gpio, value);
return (value != old);
inline bool gpioLock(GpioBase& base, unsigned char pin, bool value,
espurna::SourceLocation source_location = espurna::make_source_location())
{
if (base.valid(pin)) {
const auto old = base.lock(pin);
base.lock(pin, value);
const auto result = value != old;
gpioLockOrigin(espurna::gpio::Origin{
.base = base.id(),
.pin = pin,
.lock = value,
.result = result,
.location = trim_source_location(source_location),
});
return result;
}
return false;
}
inline bool gpioLock(GpioBase& base, unsigned char gpio) {
return gpioLock(base, gpio, true);
inline bool gpioLock(GpioBase& base, unsigned char gpio,
espurna::SourceLocation source_location = espurna::make_source_location())
{
return gpioLock(base, gpio, true, source_location);
}
inline bool gpioLock(unsigned char gpio) {
return gpioLock(hardwareGpio(), gpio);
inline bool gpioLock(unsigned char gpio,
espurna::SourceLocation source_location = espurna::make_source_location())
{
return gpioLock(hardwareGpio(), gpio, source_location);
}
inline bool gpioUnlock(GpioBase& base, unsigned char gpio) {
return gpioLock(base, gpio, false);
inline bool gpioUnlock(GpioBase& base, unsigned char gpio,
espurna::SourceLocation source_location = espurna::make_source_location())
{
return gpioLock(base, gpio, false, source_location);
}
inline bool gpioUnlock(unsigned char gpio) {
return gpioUnlock(hardwareGpio(), gpio);
inline bool gpioUnlock(unsigned char gpio,
espurna::SourceLocation source_location = espurna::make_source_location())
{
return gpioUnlock(hardwareGpio(), gpio, source_location);
}
inline bool gpioLocked(const GpioBase& base, unsigned char gpio) {
@ -99,3 +139,8 @@ inline bool gpioLocked(const GpioBase& base, unsigned char gpio) {
inline bool gpioLocked(unsigned char gpio) {
return gpioLocked(hardwareGpio(), gpio);
}
BasePinPtr gpioRegister(GpioBase& base, unsigned char gpio,
espurna::SourceLocation source_location = espurna::make_source_location());
BasePinPtr gpioRegister(unsigned char gpio,
espurna::SourceLocation source_location = espurna::make_source_location());

+ 0
- 53
code/espurna/gpio_pin.h View File

@ -1,53 +0,0 @@
/*
Part of the GPIO MODULE
Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#pragma once
#include "gpio.h"
#include <cstdint>
class GpioPin final : public BasePin {
public:
explicit GpioPin(unsigned char pin) :
_pin(pin)
{}
// ESP8266 does not have INPUT_PULLDOWN definition, and instead
// has a GPIO16-specific INPUT_PULLDOWN_16:
// - https://github.com/esp8266/Arduino/issues/478
// - https://github.com/esp8266/Arduino/commit/1b3581d55ebf0f8c91e081f9af4cf7433d492ec9
void pinMode(int8_t mode) override {
#ifdef ESP8266
if ((INPUT_PULLDOWN == mode) && (_pin == 16)) {
mode = INPUT_PULLDOWN_16;
}
#endif
::pinMode(_pin, mode);
}
void digitalWrite(int8_t val) override {
::digitalWrite(_pin, val);
}
int digitalRead() override {
return ::digitalRead(_pin);
}
unsigned char pin() const override {
return _pin;
}
const char* id() const override {
return "GpioPin";
}
private:
unsigned char _pin { GPIO_NONE };
};

+ 366
- 206
code/espurna/homeassistant.cpp View File

@ -6,7 +6,7 @@ Original module
Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
Reworked queueing and RAM usage reduction
Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
Copyright (C) 2019-2022 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
@ -14,6 +14,7 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#if HOMEASSISTANT_SUPPORT
#include "homeassistant.h"
#include "light.h"
#include "mqtt.h"
#include "relay.h"
@ -25,101 +26,167 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#include <forward_list>
#include <memory>
namespace espurna {
namespace homeassistant {
namespace {
namespace build {
PROGMEM_STRING(Prefix, HOMEASSISTANT_PREFIX);
constexpr StringView prefix() {
return Prefix;
}
constexpr bool enabled() {
return 1 == HOMEASSISTANT_ENABLED;
}
constexpr bool retain() {
return 1 == HOMEASSISTANT_RETAIN;
}
} // namespace build
namespace settings {
namespace keys {
PROGMEM_STRING(Prefix, "haPrefix");
PROGMEM_STRING(Enabled, "haEnabled");
PROGMEM_STRING(Retain, "haRetain");
} // namespace keys
String prefix() {
return getSetting(keys::Prefix, build::prefix());
}
bool enabled() {
return getSetting(keys::Enabled, build::enabled());
}
bool retain() {
return getSetting(keys::Retain, build::retain());
}
} // namespace settings
// Output is supposed to be used as both part of the MQTT config topic and the `uniq_id` field
// TODO: manage UTF8 strings? in case we somehow receive `desc`, like it was done originally
String normalize_ascii(String&& value, bool lower = false) {
auto* ptr = const_cast<char*>(value.c_str());
for (;;) {
String normalize_ascii(String value, bool lower) {
for (auto ptr = value.begin(); ptr != value.end(); ++ptr) {
switch (*ptr) {
case '\0':
goto return_value;
goto done;
case '0' ... '9':
case 'a' ... 'z':
break;
case 'A' ... 'Z':
if (lower) {
*ptr += 32;
*ptr = (*ptr + 32);
}
break;
default:
*ptr = '_';
break;
}
++ptr;
}
return_value:
return std::move(value);
done:
return value;
}
String normalize_ascii(StringView value, bool lower) {
return normalize_ascii(String(value), lower);
}
// Common data used across the discovery payloads.
// ref. https://developers.home-assistant.io/docs/entity_registry_index/
class Device {
public:
struct Strings {
Strings() = delete;
Strings(const Strings&) = delete;
Strings(Strings&&) = default;
Strings(String&& prefix_, String&& name_, const String& identifier_, const char* version_, const char* manufacturer_, const char* device_) :
prefix(std::move(prefix_)),
name(std::move(name_)),
identifier(identifier_),
version(version_),
manufacturer(manufacturer_),
device(device_)
{
name = normalize_ascii(std::move(name));
identifier = normalize_ascii(std::move(identifier), true);
}
// 'runtime' strings, may be changed in settings
struct ConfigStrings {
String name;
String identifier;
String prefix;
};
String prefix;
String name;
String identifier;
const char* version;
const char* manufacturer;
const char* device;
ConfigStrings make_config_strings() {
return ConfigStrings{
.name = normalize_ascii(systemHostname(), false),
.identifier = normalize_ascii(systemIdentifier(), true),
.prefix = settings::prefix(),
};
}
// 'build-time' strings, always the same for current build
struct BuildStrings {
String version;
String manufacturer;
String device;
};
using StringsPtr = std::unique_ptr<Strings>;
BuildStrings make_build_strings() {
BuildStrings out;
const auto app = buildApp();
out.version = String(app.version);
const auto hardware = buildHardware();
out.manufacturer = String(hardware.manufacturer);
out.device = String(hardware.device);
return out;
}
static constexpr size_t BufferSize { JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(5) };
class Device {
public:
// XXX: take care when adding / removing keys and values below
// - `const char*` is copied by pointer value, persistent pointers make sure
// it is valid for the duration of this objects lifetime
// - `F(...)` aka `__FlashStringHelpe` **will take more space**
// it is **copied inside of the buffer**, and will take `strlen()` bytes
// - allocating more objects **will silently corrupt** buffer region
// while there are *some* checks, current version is going to break
static constexpr size_t BufferSize { JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(6) };
using Buffer = StaticJsonBuffer<BufferSize>;
using BufferPtr = std::unique_ptr<Buffer>;
Device() = delete;
Device(const Device&) = delete;
Device& operator=(const Device&) = delete;
Device(Device&&) = default;
Device(String&& prefix, String&& name, const String& identifier, const char* version, const char* manufacturer, const char* device) :
_strings(std::make_unique<Strings>(std::move(prefix), std::move(name), identifier, version, manufacturer, device)),
Device(Device&&) = delete;
Device& operator=(Device&&) = delete;
Device(ConfigStrings config, BuildStrings build) :
_config(std::make_unique<ConfigStrings>(std::move(config))),
_build(std::make_unique<BuildStrings>(std::move(build))),
_buffer(std::make_unique<Buffer>()),
_root(_buffer->createObject())
{
JsonArray& ids = _root.createNestedArray("ids");
ids.add(_strings->identifier.c_str());
_root["name"] = _config->name.c_str();
auto& ids = _root.createNestedArray("ids");
ids.add(_config->identifier.c_str());
_root["name"] = _strings->name.c_str();
_root["sw"] = _strings->version;
_root["mf"] = _strings->manufacturer;
_root["mdl"] = _strings->device;
_root["sw"] = _build->version.c_str();
_root["mf"] = _build->manufacturer.c_str();
_root["mdl"] = _build->device.c_str();
}
const String& name() const {
return _strings->name;
return _config->name;
}
const String& prefix() const {
return _strings->prefix;
return _config->prefix;
}
const String& identifier() const {
return _strings->identifier;
return _config->identifier;
}
JsonObject& root() {
@ -127,7 +194,12 @@ public:
}
private:
StringsPtr _strings;
using ConfigStringsPtr = std::unique_ptr<ConfigStrings>;
ConfigStringsPtr _config;
using BuildStringsPtr = std::unique_ptr<BuildStrings>;
BuildStringsPtr _build;
BufferPtr _buffer;
JsonObject& _root;
};
@ -138,7 +210,7 @@ using JsonBufferPtr = std::unique_ptr<DynamicJsonBuffer>;
class Context {
public:
Context() = delete;
Context(DevicePtr&& device, size_t capacity) :
Context(DevicePtr device, size_t capacity) :
_device(std::move(device)),
_capacity(capacity)
{}
@ -191,19 +263,6 @@ private:
size_t _capacity { 0ul };
};
Context makeContext() {
auto device = std::make_unique<Device>(
getSetting("haPrefix", HOMEASSISTANT_PREFIX),
getSetting("hostname", getIdentifier()),
getIdentifier(),
getVersion(),
getManufacturer(),
getDevice()
);
return Context(std::move(device), 2048ul);
}
String quote(String&& value) {
if (value.equalsIgnoreCase("y")
|| value.equalsIgnoreCase("n")
@ -257,17 +316,16 @@ struct RelayContext {
RelayContext makeRelayContext() {
return {
mqttTopic(MQTT_TOPIC_STATUS, false),
mqttTopic(MQTT_TOPIC_STATUS),
quote(mqttPayloadStatus(true)),
quote(mqttPayloadStatus(false)),
quote(relayPayload(PayloadStatus::On)),
quote(relayPayload(PayloadStatus::Off))
quote(relayPayload(PayloadStatus::On).toString()),
quote(relayPayload(PayloadStatus::Off).toString())
};
}
class RelayDiscovery : public Discovery {
public:
RelayDiscovery() = delete;
explicit RelayDiscovery(Context& ctx) :
_ctx(ctx),
_relay(makeRelayContext()),
@ -283,7 +341,8 @@ public:
}
bool ok() const override {
return (_relays) && (_index < _relays);
return (_relays > 0)
&& (_index < _relays);
}
const String& uniqueId() {
@ -306,16 +365,16 @@ public:
const String& message() override {
if (!_message.length()) {
auto& json = root();
json["dev"] = _ctx.device();
json["avty_t"] = _relay.availability.c_str();
json["pl_avail"] = _relay.payload_available.c_str();
json["pl_not_avail"] = _relay.payload_not_available.c_str();
json["pl_on"] = _relay.payload_on.c_str();
json["pl_off"] = _relay.payload_off.c_str();
json["uniq_id"] = uniqueId();
json["name"] = _ctx.name() + ' ' + _index;
json["stat_t"] = mqttTopic(MQTT_TOPIC_RELAY, _index, false);
json["cmd_t"] = mqttTopic(MQTT_TOPIC_RELAY, _index, true);
json[F("dev")] = _ctx.device();
json[F("avty_t")] = _relay.availability.c_str();
json[F("pl_avail")] = _relay.payload_available.c_str();
json[F("pl_not_avail")] = _relay.payload_not_available.c_str();
json[F("pl_on")] = _relay.payload_on.c_str();
json[F("pl_off")] = _relay.payload_off.c_str();
json[F("uniq_id")] = uniqueId();
json[F("name")] = _ctx.name() + ' ' + _index;
json[F("stat_t")] = mqttTopic(MQTT_TOPIC_RELAY, _index);
json[F("cmd_t")] = mqttTopicSetter(MQTT_TOPIC_RELAY, _index);
json.printTo(_message);
}
return _message;
@ -353,21 +412,15 @@ private:
// Example payload:
// {
// "state": "ON",
// "brightness": 255,
// "color_temp": 155,
// "color_mode": "rgb",
// "color": {
// "r": 255,
// "g": 180,
// "b": 200,
// "x": 0.406,
// "y": 0.301,
// "h": 344.0,
// "s": 29.412
// },
// "effect": "colorloop",
// "state": "ON",
// "transition": 2,
// "white_value": 150
// }
// Notice that we only support JSON schema payloads, leaving it to the user to configure special
@ -375,6 +428,8 @@ private:
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
static constexpr char Topic[] = MQTT_TOPIC_LIGHT_JSON;
class LightDiscovery : public Discovery {
public:
explicit LightDiscovery(Context& ctx) :
@ -420,33 +475,45 @@ public:
if (!_message.length()) {
auto& json = root();
json["schema"] = "json";
json["uniq_id"] = uniqueId();
json[F("schema")] = "json";
json[F("uniq_id")] = uniqueId();
json["name"] = _ctx.name() + ' ' + F("Light");
json[F("name")] = _ctx.name() + ' ' + F("Light");
json["stat_t"] = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false);
json["cmd_t"] = mqttTopic(MQTT_TOPIC_LIGHT_JSON, true);
json[F("stat_t")] = mqttTopic(Topic);
json[F("cmd_t")] = mqttTopicSetter(Topic);
json["avty_t"] = mqttTopic(MQTT_TOPIC_STATUS, false);
json["pl_avail"] = quote(mqttPayloadStatus(true));
json["pl_not_avail"] = quote(mqttPayloadStatus(false));
// send `true` for every payload we support sending / receiving
// already enabled by default: "state", "transition"
json["brightness"] = true;
json[F("avty_t")] = mqttTopic(MQTT_TOPIC_STATUS);
json[F("pl_avail")] = quote(mqttPayloadStatus(true));
json[F("pl_not_avail")] = quote(mqttPayloadStatus(false));
// Note that since we send back the values immediately, HS mode sliders
// *will jump*, as calculations of input do not always match the output.
// (especially, when gamma table is used, as we modify the results)
// In case or RGB, channel values input is expected to match the output exactly.
// Since 2022.9.x we have a different payload setup
// - https://github.com/xoseperez/espurna/issues/2539
// - https://github.com/home-assistant/core/blob/2022.9.7/homeassistant/components/mqtt/light/schema_json.py
// * ignore 'onoff' and 'brightness'
// both described as 'Must be the only supported mode'
// * 'hs' is always supported, but HA UI depends on our setting and
// what gets sent in the json payload
// * 'c' and 'w' mean different things depending on *our* context
// 'rgbw' - we receive and map to 'w' to our 'warm'
// 'rgbww' - we receive and map 'c' to our 'cold' and 'w' to our 'warm'
// 'cw' / 'ww' without 'rgb' are not supported; see 'brightness' or 'color_temp'
json["brightness"] = true;
json["color_mode"] = true;
JsonArray& modes = json.createNestedArray("supported_color_modes");
if (lightHasColor()) {
if (lightUseRGB()) {
json["rgb"] = true;
} else {
json["hs"] = true;
modes.add("hs");
modes.add("rgb");
if (lightHasWarmWhite() && lightHasColdWhite()) {
modes.add("rgbww");
} else if (lightHasWarmWhite()) {
modes.add("rgbw");
}
}
@ -454,12 +521,16 @@ public:
// (...besides the internally pinned value, ref. MQTT_TOPIC_MIRED. not used here though)
// - in RGB mode, we convert the temperature into a specific color
// - in CCT mode, white channels are used
if (lightHasColor() || lightUseCCT()) {
auto range = lightMiredsRange();
if (lightHasColor() || lightHasWhite()) {
const auto range = lightMiredsRange();
json["min_mirs"] = range.cold();
json["max_mirs"] = range.warm();
json["color_temp"] = true;
modes.add("color_temp");
modes.add("white");
}
if (!modes.size()) {
modes.add("brightness");
}
json.printTo(_message);
@ -477,6 +548,33 @@ private:
String _message;
};
void heartbeat_rgb(JsonObject& root, JsonObject& color) {
const auto rgb = lightRgb();
color["r"] = rgb.red();
color["g"] = rgb.green();
color["b"] = rgb.blue();
if (lightHasWarmWhite() && lightHasColdWhite()) {
root["color_mode"] = "rgbww";
color["c"] = lightColdWhite();
color["w"] = lightWarmWhite();
} else if (lightHasWarmWhite()) {
root["color_mode"] = "rgbw";
color["w"] = lightWarmWhite();
} else {
root["color_mode"] = "rgb";
}
}
void heartbeat_hsv(JsonObject& root, JsonObject& color) {
root["color_mode"] = "hs";
const auto hsv = lightHsv();
color["h"] = hsv.hue();
color["s"] = hsv.saturation();
}
bool heartbeat(heartbeat::Mask mask) {
// TODO: mask json payload specifically?
// or, find a way to detach masking from the system setting / don't use heartbeat timer
@ -484,27 +582,17 @@ bool heartbeat(heartbeat::Mask mask) {
DynamicJsonBuffer buffer(512);
JsonObject& root = buffer.createObject();
auto state = lightState();
const auto state = lightState();
root["state"] = state ? "ON" : "OFF";
if (state) {
root["brightness"] = lightBrightness();
if (lightUseCCT()) {
root["white_value"] = lightColdWhite();
}
if (lightColor()) {
if (lightHasColor() && lightColor()) {
auto& color = root.createNestedObject("color");
if (lightUseRGB()) {
auto rgb = lightRgb();
color["r"] = rgb.red();
color["g"] = rgb.green();
color["b"] = rgb.blue();
heartbeat_rgb(root, color);
} else {
auto hsv = lightHsv();
color["h"] = hsv.hue();
color["s"] = hsv.saturation();
heartbeat_hsv(root, color);
}
}
}
@ -512,8 +600,10 @@ bool heartbeat(heartbeat::Mask mask) {
String message;
root.printTo(message);
String topic = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false);
mqttSendRaw(topic.c_str(), message.c_str(), false);
static const auto topic = StringView(Topic).toString();
mqttSendRaw(
mqttTopic(topic).c_str(),
message.c_str(), false);
}
return true;
@ -523,9 +613,9 @@ void publishLightJson() {
heartbeat(static_cast<heartbeat::Mask>(heartbeat::Report::Light));
}
void receiveLightJson(char* payload) {
void receiveLightJson(StringView payload) {
DynamicJsonBuffer buffer(1024);
JsonObject& root = buffer.parseObject(payload);
JsonObject& root = buffer.parseObject(payload.begin());
if (!root.success()) {
return;
}
@ -534,25 +624,30 @@ void receiveLightJson(char* payload) {
return;
}
auto state = root["state"].as<String>();
if (state == F("ON")) {
const auto state = root["state"].as<String>();
if (StringView("ON") == state) {
lightState(true);
} else if (state == F("OFF")) {
} else if (StringView("OFF") == state) {
lightState(false);
} else {
return;
}
unsigned long transition { lightTransitionTime() };
auto transition = lightTransitionTime();
if (root.containsKey("transition")) {
auto seconds = root["transition"].as<float>();
if (seconds > 0) {
transition = static_cast<unsigned long>(seconds * 1000.0);
using LocalUnit = decltype(lightTransitionTime());
using RemoteUnit = std::chrono::duration<float>;
auto seconds = RemoteUnit(root["transition"].as<float>());
if (seconds.count() > 0.0f) {
transition = std::chrono::duration_cast<LocalUnit>(seconds);
}
}
if (root.containsKey("color_temp")) {
lightMireds(root["color_temp"].as<long>());
const auto mireds = root["color_temp"].as<long>();
lightTemperature(light::Mireds{ .value = mireds });
}
if (root.containsKey("brightness")) {
@ -561,20 +656,29 @@ void receiveLightJson(char* payload) {
if (lightHasColor() && root.containsKey("color")) {
JsonObject& color = root["color"];
if (lightUseRGB()) {
if (color.containsKey("h")
&& color.containsKey("s"))
{
lightHs(
color["h"].as<long>(),
color["s"].as<long>());
} else if (color.containsKey("r")
&& color.containsKey("g")
&& color.containsKey("b"))
{
lightRgb({
color["r"].as<long>(),
color["g"].as<long>(),
color["b"].as<long>()});
} else {
lightHs(
color["h"].as<long>(),
color["s"].as<long>());
}
}
if (lightUseCCT() && root.containsKey("white_value")) {
lightColdWhite(root["white_value"].as<long>());
if (color.containsKey("w")) {
lightWarmWhite(color["w"].as<long>());
}
if (color.containsKey("c")) {
lightColdWhite(color["c"].as<long>());
}
}
lightUpdate({transition, lightTransitionStep()});
@ -586,11 +690,14 @@ void receiveLightJson(char* payload) {
class SensorDiscovery : public Discovery {
public:
SensorDiscovery() = delete;
explicit SensorDiscovery(Context& ctx) :
_ctx(ctx),
_magnitudes(magnitudeCount())
{}
{
if (_magnitudes > 0) {
_info = magnitudeInfo(_index);
}
}
JsonObject& root() {
if (!_root) {
@ -600,8 +707,9 @@ public:
return *_root;
}
bool ok() const {
return _index < _magnitudes;
bool ok() const override {
return (_magnitudes > 0)
&& (_index < _magnitudes);
}
const String& topic() override {
@ -618,12 +726,12 @@ public:
const String& message() override {
if (!_message.length()) {
auto& json = root();
json["dev"] = _ctx.device();
json["uniq_id"] = uniqueId();
json[F("dev")] = _ctx.device();
json[F("uniq_id")] = uniqueId();
json["name"] = _ctx.name() + ' ' + name() + ' ' + localId();
json["stat_t"] = mqttTopic(magnitudeTopicIndex(_index), false);
json["unit_of_meas"] = magnitudeUnits(_index);
json[F("name")] = _ctx.name() + ' ' + name() + ' ' + localId();
json[F("stat_t")] = mqttTopic(_info.topic);
json[F("unit_of_meas")] = magnitudeUnitsName(_info.units);
json.printTo(_message);
}
@ -633,14 +741,14 @@ public:
const String& name() {
if (!_name.length()) {
_name = magnitudeTopic(magnitudeType(_index));
_name = magnitudeTypeTopic(_info.type);
}
return _name;
}
unsigned char localId() const {
return magnitudeIndex(_index);
return _info.index;
}
const String& uniqueId() {
@ -656,6 +764,7 @@ public:
auto current = _index;
++_index;
if ((_index > current) && (_index < _magnitudes)) {
_info = magnitudeInfo(_index);
_unique_id = "";
_name = "";
_topic = "";
@ -671,8 +780,9 @@ private:
Context& _ctx;
JsonObject* _root { nullptr };
unsigned char _index { 0u };
unsigned char _magnitudes { 0u };
unsigned char _index { 0u };
sensor::Info _info;
String _unique_id;
String _name;
@ -682,20 +792,38 @@ private:
#endif
DevicePtr make_device_ptr() {
return std::make_unique<Device>(
make_config_strings(),
make_build_strings());
}
Context make_context() {
return Context(make_device_ptr(), 2048);
}
// Reworked discovery class. Try to send and wait for MQTT QoS 1 publish ACK to continue.
// Topic and message are generated on demand and most of JSON payload is cached for re-use to save RAM.
class DiscoveryTask {
public:
using Entity = std::unique_ptr<Discovery>;
using Entities = std::forward_list<Entity>;
static constexpr duration::Milliseconds WaitShort { 100 };
static constexpr duration::Milliseconds WaitLong { 1000 };
static constexpr int Retries { 5 };
static constexpr unsigned long WaitShortMs { 100ul };
static constexpr unsigned long WaitLongMs { 1000ul };
DiscoveryTask(bool enabled) :
_enabled(enabled)
DiscoveryTask() = delete;
DiscoveryTask(const DiscoveryTask&) = delete;
DiscoveryTask& operator=(const DiscoveryTask&) = delete;
DiscoveryTask(DiscoveryTask&&) = delete;
DiscoveryTask& operator=(DiscoveryTask&&) = delete;
DiscoveryTask(Context ctx, bool enabled) :
_enabled(enabled),
_ctx(std::move(ctx))
{}
void add(Entity&& entity) {
@ -764,12 +892,15 @@ public:
private:
bool _enabled { false };
int _retry { Retries };
Context _ctx { makeContext() };
Entities _entities;
Context _ctx;
};
constexpr duration::Milliseconds DiscoveryTask::WaitShort;
constexpr duration::Milliseconds DiscoveryTask::WaitLong;
namespace internal {
using TaskPtr = std::shared_ptr<DiscoveryTask>;
@ -785,12 +916,12 @@ enum class State {
};
State state { State::Initial };
Ticker timer;
timer::SystemTimer timer;
void send(TaskPtr ptr, FlagPtr flag_ptr);
void stop(bool done) {
timer.detach();
timer.stop();
if (done) {
DEBUG_MSG_P(PSTR("[HA] Stopping discovery\n"));
state = State::Sent;
@ -800,18 +931,20 @@ void stop(bool done) {
}
}
void schedule(unsigned long wait, TaskPtr ptr, FlagPtr flag_ptr) {
internal::timer.once_ms_scheduled(wait, [ptr, flag_ptr]() {
send(ptr, flag_ptr);
});
void schedule(duration::Milliseconds wait, TaskPtr ptr, FlagPtr flag_ptr) {
timer.schedule_once(
wait,
[ptr, flag_ptr]() {
send(ptr, flag_ptr);
});
}
void schedule(TaskPtr ptr, FlagPtr flag_ptr) {
schedule(DiscoveryTask::WaitShortMs, ptr, flag_ptr);
schedule(DiscoveryTask::WaitShort, ptr, flag_ptr);
}
void schedule(TaskPtr ptr) {
schedule(DiscoveryTask::WaitShortMs, ptr, std::make_shared<bool>(true));
schedule(DiscoveryTask::WaitShort, ptr, std::make_shared<bool>(true));
}
void send(TaskPtr ptr, FlagPtr flag_ptr) {
@ -853,8 +986,8 @@ void send(TaskPtr ptr, FlagPtr flag_ptr) {
#endif
auto wait = res
? DiscoveryTask::WaitShortMs
: DiscoveryTask::WaitLongMs;
? DiscoveryTask::WaitShort
: DiscoveryTask::WaitLong;
if (res || task.retry()) {
schedule(wait, ptr, flag_ptr);
@ -867,11 +1000,12 @@ void send(TaskPtr ptr, FlagPtr flag_ptr) {
} // namespace internal
void publishDiscovery() {
if (!mqttConnected() || internal::timer.active() || (internal::state != internal::State::Pending)) {
if (!mqttConnected() || internal::timer || (internal::state != internal::State::Pending)) {
return;
}
auto task = std::make_shared<DiscoveryTask>(internal::enabled);
auto task = std::make_shared<DiscoveryTask>(
make_context(), internal::enabled);
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
task->add<LightDiscovery>();
@ -893,8 +1027,8 @@ void publishDiscovery() {
void configure() {
bool current = internal::enabled;
internal::enabled = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
internal::retain = getSetting("haRetain", 1 == HOMEASSISTANT_RETAIN);
internal::enabled = settings::enabled();
internal::retain = settings::retain();
if (internal::enabled != current) {
internal::state = internal::State::Pending;
@ -903,27 +1037,27 @@ void configure() {
homeassistant::publishDiscovery();
}
void mqttCallback(unsigned int type, const char* topic, char* payload) {
void mqttCallback(unsigned int type, StringView topic, StringView payload) {
if (MQTT_DISCONNECT_EVENT == type) {
if (internal::state == internal::State::Sent) {
internal::state = internal::State::Pending;
}
internal::timer.detach();
internal::timer.stop();
return;
}
if (MQTT_CONNECT_EVENT == type) {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
::mqttSubscribe(MQTT_TOPIC_LIGHT_JSON);
::mqttSubscribe(Topic);
#endif
::schedule_function(publishDiscovery);
::espurnaRegisterOnce(publishDiscovery);
return;
}
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
if (type == MQTT_MESSAGE_EVENT) {
String t = ::mqttMagnitude(topic);
if (t.equals(MQTT_TOPIC_LIGHT_JSON)) {
auto t = ::mqttMagnitude(topic);
if (t.equals(Topic)) {
receiveLightJson(payload);
}
return;
@ -935,56 +1069,82 @@ namespace web {
#if WEB_SUPPORT
PROGMEM_STRING(Prefix, "ha");
void onVisible(JsonObject& root) {
root["haVisible"] = 1;
wsPayloadModule(root, Prefix);
}
void onConnected(JsonObject& root) {
root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
root["haEnabled"] = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
root["haRetain"] = getSetting("haRetain", 1 == HOMEASSISTANT_RETAIN);
root[FPSTR(settings::keys::Prefix)] = settings::prefix();
root[FPSTR(settings::keys::Enabled)] = settings::enabled();
root[FPSTR(settings::keys::Retain)] = settings::retain();
}
bool onKeyCheck(const char* key, JsonVariant& value) {
return (strncmp(key, "ha", 2) == 0);
bool onKeyCheck(StringView key, const JsonVariant& value) {
return espurna::settings::query::samePrefix(key, Prefix);
}
#endif
} // namespace web
} // namespace homeassistant
// This module no longer implements .yaml generation, since we can't:
// - use unique_id in the device config
// - have abbreviated keys
// - have mqtt reliably return the correct status & command payloads when it is disabled
// (yet? needs reworked configuration section or making functions read settings directly)
#if TERMINAL_SUPPORT
namespace terminal {
void haSetup() {
PROGMEM_STRING(Send, "HA.SEND");
void send(::terminal::CommandContext&& ctx) {
internal::state = internal::State::Pending;
publishDiscovery();
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Send, send},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
#endif
void setup() {
#if WEB_SUPPORT
wsRegister()
.onVisible(homeassistant::web::onVisible)
.onConnected(homeassistant::web::onConnected)
.onKeyCheck(homeassistant::web::onKeyCheck);
.onVisible(web::onVisible)
.onConnected(web::onConnected)
.onKeyCheck(web::onKeyCheck);
#endif
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
lightSetReportListener(homeassistant::publishLightJson);
mqttHeartbeat(homeassistant::heartbeat);
lightOnReport(publishLightJson);
mqttHeartbeat(heartbeat);
#endif
mqttRegister(homeassistant::mqttCallback);
mqttRegister(mqttCallback);
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("HA.SEND"), [](const terminal::CommandContext& ctx) {
using namespace homeassistant::internal;
state = State::Pending;
homeassistant::publishDiscovery();
terminalOK(ctx);
});
terminal::setup();
#endif
espurnaRegisterReload(homeassistant::configure);
homeassistant::configure();
espurnaRegisterReload(configure);
configure();
}
} // namespace
} // namespace homeassistant
} // namespace espurna
// This module no longer implements .yaml generation, since we can't:
// - use unique_id in the device config
// - have abbreviated keys
// - have mqtt reliably return the correct status & command payloads when it is disabled
// (yet? needs reworked configuration section or making functions read settings directly)
void haSetup() {
espurna::homeassistant::setup();
}
#endif // HOMEASSISTANT_SUPPORT

+ 0
- 2
code/espurna/homeassistant.h View File

@ -8,6 +8,4 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#pragma once
#include "espurna.h"
void haSetup();

+ 350
- 154
code/espurna/i2c.cpp View File

@ -6,55 +6,222 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "i2c.h"
#include "espurna.h"
#if I2C_SUPPORT
#include <Wire.h>
unsigned int _i2c_locked[16] = {0};
#if I2C_USE_BRZO
#include <brzo_i2c.h>
unsigned long _i2c_scl_frequency = 0;
#else
#include <Wire.h>
#endif
#include "i2c.h"
#include <cstring>
#include <bitset>
// -----------------------------------------------------------------------------
// Private
// -----------------------------------------------------------------------------
int _i2cGetSDA() {
return getSetting("i2cSDA", I2C_SDA_PIN);
namespace espurna {
namespace i2c {
namespace {
struct Bus {
unsigned char sda { GPIO_NONE };
unsigned char scl { GPIO_NONE };
#if I2C_USE_BRZO
unsigned long frequency { 0 };
#endif
};
namespace internal {
Bus bus;
} // namespace internal
namespace lock {
std::bitset<128> storage{};
void reset(uint8_t address) {
storage.reset(address);
}
bool get(uint8_t address) {
return storage.test(address);
}
bool set(uint8_t address) {
if (!get(address)) {
storage.set(address);
return true;
}
return false;
}
} // namespace lock
#if I2C_USE_BRZO
void brzo_i2c_start_transaction(uint8_t address) {
::brzo_i2c_start_transaction(address, internal::bus.frequency);
}
#endif
namespace build {
constexpr unsigned char sda() {
return I2C_SDA_PIN;
}
constexpr unsigned char scl() {
return I2C_SCL_PIN;
}
constexpr bool performScanOnBoot() {
return I2C_PERFORM_SCAN == 1;
}
#if I2C_USE_BRZO
constexpr unsigned long cst() {
return I2C_CLOCK_STRETCH_TIME;
}
int _i2cGetSCL() {
return getSetting("i2cSCL", I2C_SCL_PIN);
constexpr unsigned long sclFrequency() {
return I2C_SCL_FREQUENCY;
}
#endif
} // namespace build
namespace settings {
int _i2cClearbus(int sda, int scl) {
unsigned char sda() {
return getSetting("i2cSDA", build::sda());
}
unsigned char scl() {
return getSetting("i2cSCL", build::scl());
}
#if I2C_USE_BRZO
unsigned long cst() {
return getSetting("i2cCST", build::cst());
}
unsigned long sclFrequency() {
return getSetting("i2cFreq", build::sclFrequency());
}
#endif
#if defined(TWCR) && defined(TWEN)
// Disable the Atmel 2-Wire interface so we can control the SDA and SCL pins directly
TWCR &= ~(_BV(TWEN));
#endif
} // namespace settings
// make note that both APIs return integer status codes
// success is 0, everything else depends on the implementation
// for example, for our Wire it is:
// - 4 if line is busy
// - 2 if NACK happened when writing address
// - 3 if NACK happened when writing data
bool find(uint8_t address) {
#if I2C_USE_BRZO
i2c::start_brzo_transaction(address);
brzo_i2c_ACK_polling(1000);
return 0 == brzo_i2c_end_transaction();
#else
Wire.beginTransmission(address);
return 0 == Wire.endTransmission();
#endif
}
template <typename T>
uint8_t find(const uint8_t* begin, const uint8_t* end, T&& filter) {
for (const auto* it = begin; it != end; ++it) {
if (filter(*it) && find(*it)) {
return *it;
}
}
return 0;
}
uint8_t find(const uint8_t* begin, const uint8_t* end) {
return find(begin, end, [](uint8_t) {
return true;
});
}
uint8_t findAndLock(const uint8_t* begin, const uint8_t* end) {
const auto address = find(begin, end, [](uint8_t address) {
return !lock::get(address);
});
if (address != 0) {
lock::set(address);
}
return address;
}
template <typename T>
void scan(T&& callback) {
static constexpr uint8_t Min { 0x8 };
static constexpr uint8_t Max { 0x78 };
for (auto address = Min; address < Max; ++address) {
if (find(address)) {
callback(address);
}
}
}
void bootScan() {
String addresses;
scan([&](uint8_t address) {
if (addresses.length()) {
addresses += F(", ");
}
addresses += F("0x");
addresses += hexEncode(address);
});
if (addresses.length()) {
DEBUG_MSG_P(PSTR("[I2C] Found device(s): %s\n"), addresses.c_str());
} else {
DEBUG_MSG_P(PSTR("[I2C] No devices found\n"));
}
}
int clear(unsigned char sda, unsigned char scl) {
#if defined(TWCR) && defined(TWEN)
// Disable the Atmel 2-Wire interface so we can control the SDA and SCL pins directly
TWCR &= ~(_BV(TWEN));
#endif
// Make SDA (data) and SCL (clock) pins inputs with pullup
pinMode(sda, INPUT_PULLUP);
pinMode(scl, INPUT_PULLUP);
nice_delay(2500);
// Wait 2.5 secs. This is strictly only necessary on the first power
// up of the DS3231 module to allow it to initialize properly,
// but is also assists in reliable programming of FioV3 boards as it gives the
// IDE a chance to start uploaded the program
// before existing sketch confuses the IDE by sending Serial data.
espurna::time::blockingDelay(
espurna::duration::Milliseconds(2500));
// If it is held low the device cannot become the I2C master
// I2C bus error. Could not clear SCL clock line held low
boolean scl_low = (digitalRead(scl) == LOW);
if (scl_low) return 1;
bool scl_low = (digitalRead(scl) == LOW);
if (scl_low) {
return 1;
}
boolean sda_low = (digitalRead(sda) == LOW);
bool sda_low = (digitalRead(sda) == LOW);
int clockCount = 20; // > 2x9 clock
// While SDA is low for at most 20 cycles
@ -77,13 +244,16 @@ int _i2cClearbus(int sda, int scl) {
int counter = 20;
while (scl_low && (counter > 0)) {
counter--;
nice_delay(100);
espurna::time::blockingDelay(
espurna::duration::Milliseconds(100));
scl_low = (digitalRead(scl) == LOW);
}
// If still low after 2 sec error
// I2C bus error. Could not clear. SCL clock line held low by slave clock stretch for >2sec
if (scl_low) return 2;
if (scl_low) {
return 2;
}
sda_low = (digitalRead(sda) == LOW); // and check SDA input again and loop
@ -91,7 +261,9 @@ int _i2cClearbus(int sda, int scl) {
// If still low
// I2C bus error. Could not clear. SDA data line held low
if (sda_low) return 3;
if (sda_low) {
return 3;
}
// Pull SDA line low for "start" or "repeated start"
pinMode(sda, INPUT); // remove pullup
@ -110,9 +282,92 @@ int _i2cClearbus(int sda, int scl) {
// Everything OK
return 0;
}
int clear(const Bus& bus) {
return clear(bus.sda, bus.scl);
}
int clear() {
return clear(internal::bus);
}
void init() {
internal::bus.sda = settings::sda();
internal::bus.scl = settings::scl();
#if I2C_USE_BRZO
internal::bus.frequency = settings::sclFrequency();
brzo_i2c_setup(internal::bus.sda, internal::bus.scl, settings::cst());
#else
Wire.begin(internal::bus.sda, internal::bus.scl);
#endif
DEBUG_MSG_P(PSTR("[I2C] Initialized SDA @ GPIO%hhu and SCL @ GPIO%hhu\n"),
internal::bus.sda, internal::bus.scl);
#if I2C_CLEAR_BUS
clear(internal::bus);
#endif
}
#if TERMINAL_SUPPORT
namespace terminal {
PROGMEM_STRING(Locked, "I2C.LOCKED");
void locked(::terminal::CommandContext&& ctx) {
for (size_t address = 0; address < lock::storage.size(); ++address) {
if (lock::storage.test(address)) {
ctx.output.printf_P(PSTR("0x%02X\n"), address);
}
}
terminalOK(ctx);
}
PROGMEM_STRING(Scan, "I2C.SCAN");
void scan(::terminal::CommandContext&& ctx) {
size_t devices { 0 };
i2c::scan([&](uint8_t address) {
++devices;
ctx.output.printf_P(PSTR("0x%02X\n"), address);
});
if (devices) {
ctx.output.printf_P(PSTR("found %zu device(s)\n"), devices);
terminalOK(ctx);
return;
}
terminalError(ctx, F("no devices found"));
}
PROGMEM_STRING(Clear, "I2C.CLEAR");
void clear(::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("result %d\n"), i2c::clear());
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Locked, locked},
{Scan, scan},
{Clear, clear},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
#endif // TERMINAL_SUPPORT
} // namespace
} // namespace i2c
} // namespace espurna
// ---------------------------------------------------------------------
// I2C API
// ---------------------------------------------------------------------
@ -120,12 +375,12 @@ int _i2cClearbus(int sda, int scl) {
#if I2C_USE_BRZO
void i2c_wakeup(uint8_t address) {
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
i2c::brzo_i2c_start_transaction(address);
brzo_i2c_end_transaction();
}
uint8_t i2c_write_buffer(uint8_t address, uint8_t * buffer, size_t len) {
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
i2c::brzo_i2c_start_transaction(address);
brzo_i2c_write(buffer, len, false);
return brzo_i2c_end_transaction();
}
@ -137,40 +392,40 @@ uint8_t i2c_write_uint8(uint8_t address, uint8_t value) {
uint8_t i2c_read_uint8(uint8_t address) {
uint8_t buffer[1] = {0};
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
i2c::brzo_i2c_start_transaction(address);
brzo_i2c_read(buffer, 1, false);
brzo_i2c_end_transaction();
return buffer[0];
};
}
uint8_t i2c_read_uint8(uint8_t address, uint8_t reg) {
uint8_t buffer[1] = {reg};
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
i2c::brzo_i2c_start_transaction(address);
brzo_i2c_write(buffer, 1, true);
brzo_i2c_read(buffer, 1, false);
brzo_i2c_end_transaction();
return buffer[0];
};
}
uint16_t i2c_read_uint16(uint8_t address) {
uint8_t buffer[2] = {0, 0};
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
i2c::brzo_i2c_start_transaction(address);
brzo_i2c_read(buffer, 2, false);
brzo_i2c_end_transaction();
return (buffer[0] * 256) | buffer[1];
};
}
uint16_t i2c_read_uint16(uint8_t address, uint8_t reg) {
uint8_t buffer[2] = {reg, 0};
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
i2c::brzo_i2c_start_transaction(address);
brzo_i2c_write(buffer, 1, true);
brzo_i2c_read(buffer, 2, false);
brzo_i2c_end_transaction();
return (buffer[0] * 256) | buffer[1];
};
}
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len) {
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
i2c::start_brzo_transaction(address);
brzo_i2c_read(buffer, len, false);
brzo_i2c_end_transaction();
}
@ -196,12 +451,10 @@ uint8_t i2c_write_buffer(uint8_t address, uint8_t * buffer, size_t len) {
uint8_t i2c_read_uint8(uint8_t address) {
uint8_t value;
Wire.beginTransmission((uint8_t) address);
Wire.requestFrom((uint8_t) address, (uint8_t) 1);
value = Wire.read();
Wire.endTransmission();
return value;
};
}
uint8_t i2c_read_uint8(uint8_t address, uint8_t reg) {
uint8_t value;
@ -210,18 +463,15 @@ uint8_t i2c_read_uint8(uint8_t address, uint8_t reg) {
Wire.endTransmission();
Wire.requestFrom((uint8_t) address, (uint8_t) 1);
value = Wire.read();
Wire.endTransmission();
return value;
};
}
uint16_t i2c_read_uint16(uint8_t address) {
uint16_t value;
Wire.beginTransmission((uint8_t) address);
Wire.requestFrom((uint8_t) address, (uint8_t) 2);
value = (Wire.read() * 256) | Wire.read();
Wire.endTransmission();
return value;
};
}
uint16_t i2c_read_uint16(uint8_t address, uint8_t reg) {
uint16_t value;
@ -230,15 +480,46 @@ uint16_t i2c_read_uint16(uint8_t address, uint8_t reg) {
Wire.endTransmission();
Wire.requestFrom((uint8_t) address, (uint8_t) 2);
value = (Wire.read() * 256) | Wire.read();
Wire.endTransmission();
return value;
};
}
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len) {
Wire.beginTransmission((uint8_t) address);
void i2c_read_buffer(uint8_t address, uint8_t* buffer, size_t len) {
Wire.requestFrom(address, (uint8_t) len);
for (size_t i=0; i<len; i++) buffer[i] = Wire.read();
Wire.endTransmission();
for (size_t i=0; i<len; ++i) {
buffer[i] = Wire.read();
}
}
void i2c_write_uint(uint8_t address, uint16_t reg, uint32_t input, size_t size) {
if (size && (size <= sizeof(input))) {
Wire.beginTransmission(address);
Wire.write((reg >> 8) & 0xff);
Wire.write(reg & 0xff);
uint8_t buf[sizeof(input)];
std::memcpy(&buf[0], &input, sizeof(buf));
Wire.write(&buf[sizeof(buf) - size], size);
Wire.endTransmission();
}
}
uint32_t i2c_read_uint(uint8_t address, uint16_t reg, size_t size, bool stop) {
uint32_t out { 0 };
if (size <= sizeof(out)) {
Wire.beginTransmission(address);
Wire.write((reg >> 8) & 0xff);
Wire.write(reg & 0xff);
Wire.endTransmission(stop);
if (size == Wire.requestFrom(address, size)) {
for (size_t byte = 0; byte < size; --byte) {
out = (out << 8ul) | static_cast<uint8_t>(Wire.read());
}
}
}
return out;
}
#endif // I2C_USE_BRZO
@ -271,139 +552,54 @@ uint8_t i2c_write_uint16(uint8_t address, uint16_t value) {
uint16_t i2c_read_uint16_le(uint8_t address, uint8_t reg) {
uint16_t temp = i2c_read_uint16(address, reg);
return (temp / 256) | (temp * 256);
};
}
int16_t i2c_read_int16(uint8_t address, uint8_t reg) {
return (int16_t) i2c_read_uint16(address, reg);
};
}
int16_t i2c_read_int16_le(uint8_t address, uint8_t reg) {
return (int16_t) i2c_read_uint16_le(address, reg);
};
}
// -----------------------------------------------------------------------------
// Utils
// -----------------------------------------------------------------------------
void i2cClearBus() {
DEBUG_MSG_P(
PSTR("[I2C] Clear bus (response: %d)\n"),
_i2cClearbus(_i2cGetSDA(), _i2cGetSCL())
);
int i2cClearBus() {
return espurna::i2c::clear();
}
bool i2cCheck(unsigned char address) {
#if I2C_USE_BRZO
brzo_i2c_start_transaction(address, _i2c_scl_frequency);
brzo_i2c_ACK_polling(1000);
return brzo_i2c_end_transaction();
#else
Wire.beginTransmission(address);
return Wire.endTransmission();
#endif
bool i2cLock(uint8_t address) {
return espurna::i2c::lock::set(address);
}
bool i2cGetLock(unsigned char address) {
unsigned char index = address / 8;
unsigned char mask = 1 << (address % 8);
if (_i2c_locked[index] & mask) return false;
_i2c_locked[index] = _i2c_locked[index] | mask;
DEBUG_MSG_P(PSTR("[I2C] Address 0x%02X locked\n"), address);
return true;
void i2cUnlock(uint8_t address) {
espurna::i2c::lock::reset(address);
}
bool i2cReleaseLock(unsigned char address) {
unsigned char index = address / 8;
unsigned char mask = 1 << (address % 8);
if (_i2c_locked[index] & mask) {
_i2c_locked[index] = _i2c_locked[index] & ~mask;
return true;
}
return false;
uint8_t i2cFind(uint8_t address) {
return espurna::i2c::find(address);
}
unsigned char i2cFind(size_t size, unsigned char * addresses, unsigned char &start) {
for (unsigned char i=start; i<size; i++) {
if (i2cCheck(addresses[i]) == 0) {
start = i;
return addresses[i];
}
}
return 0;
uint8_t i2cFind(const uint8_t* begin, const uint8_t* end) {
return espurna::i2c::find(begin, end);
}
unsigned char i2cFind(size_t size, unsigned char * addresses) {
unsigned char start = 0;
return i2cFind(size, addresses, start);
uint8_t i2cFindAndLock(const uint8_t* begin, const uint8_t* end) {
return espurna::i2c::findAndLock(begin, end);
}
unsigned char i2cFindAndLock(size_t size, unsigned char * addresses) {
unsigned char start = 0;
unsigned char address = 0;
while ((address = i2cFind(size, addresses, start))) {
if (i2cGetLock(address)) break;
start++;
}
return address;
}
void i2cScan() {
unsigned char nDevices = 0;
for (unsigned char address = 1; address < 127; address++) {
unsigned char error = i2cCheck(address);
if (error == 0) {
DEBUG_MSG_P(PSTR("[I2C] Device found at address 0x%02X\n"), address);
nDevices++;
}
}
if (nDevices == 0) DEBUG_MSG_P(PSTR("[I2C] No devices found\n"));
}
#if TERMINAL_SUPPORT
void _i2cInitCommands() {
terminalRegisterCommand(F("I2C.SCAN"), [](const terminal::CommandContext&) {
i2cScan();
terminalOK();
});
terminalRegisterCommand(F("I2C.CLEAR"), [](const terminal::CommandContext&) {
i2cClearBus();
terminalOK();
});
}
#endif // TERMINAL_SUPPORT
void i2cSetup() {
espurna::i2c::init();
const auto sda = _i2cGetSDA();
const auto scl = _i2cGetSCL();
#if I2C_USE_BRZO
auto cst = getSetting("i2cCST", I2C_CLOCK_STRETCH_TIME);
_i2c_scl_frequency = getSetting("i2cFreq", I2C_SCL_FREQUENCY);
brzo_i2c_setup(sda, scl, cst);
#else
Wire.begin(sda, scl);
#endif
DEBUG_MSG_P(PSTR("[I2C] Using GPIO%02d for SDA and GPIO%02d for SCL\n"), sda, scl);
#if TERMINAL_SUPPORT
_i2cInitCommands();
#endif
#if I2C_CLEAR_BUS
i2cClearBus();
#endif
#if I2C_PERFORM_SCAN
i2cScan();
#endif
#if TERMINAL_SUPPORT
espurna::i2c::terminal::setup();
#endif
if (espurna::i2c::build::performScanOnBoot()) {
espurna::i2c::bootScan();
}
}
#endif

+ 13
- 9
code/espurna/i2c.h View File

@ -8,7 +8,8 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#pragma once
#include "espurna.h"
#include <cstddef>
#include <cstdint>
void i2c_wakeup(uint8_t address);
uint8_t i2c_write_buffer(uint8_t address, uint8_t * buffer, size_t len);
@ -24,15 +25,18 @@ uint16_t i2c_read_uint16(uint8_t address, uint8_t reg);
uint16_t i2c_read_uint16_le(uint8_t address, uint8_t reg);
int16_t i2c_read_int16(uint8_t address, uint8_t reg);
int16_t i2c_read_int16_le(uint8_t address, uint8_t reg);
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len);
void i2cScan();
void i2cClearBus();
bool i2cGetLock(unsigned char address);
bool i2cReleaseLock(unsigned char address);
void i2c_read_buffer(uint8_t address, uint8_t* buffer, size_t len);
uint32_t i2c_read_uint(uint8_t address, uint16_t reg, size_t len, bool stop);
void i2c_write_uint(uint8_t address, uint16_t reg, uint32_t input, size_t len);
unsigned char i2cFindAndLock(size_t size, unsigned char * addresses);
unsigned char i2cFind(size_t size, unsigned char * addresses, unsigned char &start);
unsigned char i2cFind(size_t size, unsigned char * addresses);
uint8_t i2cFind(uint8_t);
bool i2cLock(uint8_t address);
void i2cUnlock(uint8_t address);
uint8_t i2cFind(const uint8_t* begin, const uint8_t* end);
uint8_t i2cFindAndLock(const uint8_t* begin, const uint8_t* end);
int i2cClearBus();
void i2cSetup();

+ 355
- 216
code/espurna/ifan.cpp View File

@ -24,230 +24,354 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
// TODO: in case there are more FANs, move externally
namespace ifan02 {
const char* speedToPayload(FanSpeed value) {
switch (value) {
case FanSpeed::Off:
return "off";
case FanSpeed::Low:
return "low";
case FanSpeed::Medium:
return "medium";
case FanSpeed::High:
return "high";
}
return "";
}
FanSpeed payloadToSpeed(const char* payload) {
auto len = strlen(payload);
if (len == 1) {
switch (payload[0]) {
case '0':
return FanSpeed::Off;
case '1':
return FanSpeed::Low;
case '2':
return FanSpeed::Medium;
case '3':
return FanSpeed::High;
}
} else if (len > 1) {
String cmp(payload);
if (cmp == "off") {
return FanSpeed::Off;
} else if (cmp == "low") {
return FanSpeed::Low;
} else if (cmp == "medium") {
return FanSpeed::Medium;
} else if (cmp == "high") {
return FanSpeed::High;
}
}
return FanSpeed::Off;
}
FanSpeed payloadToSpeed(const String& string) {
return payloadToSpeed(string.c_str());
}
namespace espurna {
namespace settings {
namespace options {
namespace {
PROGMEM_STRING(Off, "off");
PROGMEM_STRING(Low, "low");
PROGMEM_STRING(Medium, "medium");
PROGMEM_STRING(High, "high");
static constexpr std::array<espurna::settings::options::Enumeration<FanSpeed>, 4> FanSpeedOptions PROGMEM {
{{FanSpeed::Off, Off},
{FanSpeed::Low, Low},
{FanSpeed::Medium, Medium},
{FanSpeed::High, High}}
};
} // namespace ifan02
} // namespace
} // namespace options
namespace settings {
namespace internal {
template <>
FanSpeed convert(const String& value) {
return ifan02::payloadToSpeed(value);
return convert(options::FanSpeedOptions, value, FanSpeed::Medium);
}
String serialize(FanSpeed speed) {
return serialize(options::FanSpeedOptions, speed);
}
} // namespace internal
} // namespace settings
namespace ifan02 {
namespace {
constexpr unsigned long DefaultSaveDelay { 1000ul };
FanSpeed payloadToSpeed(const String& value) {
return espurna::settings::internal::convert<FanSpeed>(value);
}
// We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed
// Sync up with the relay and write it on ON / OFF status events
String speedToPayload(FanSpeed speed) {
return espurna::settings::internal::serialize(speed);
}
namespace build {
static constexpr auto ControlPin = uint8_t{ 12 };
constexpr size_t Gpios { 3ul };
static constexpr auto SaveDelay = duration::Seconds{ 10 };
static constexpr auto Speed = FanSpeed::Medium;
using State = std::array<int8_t, Gpios>;
} // namespace build
using Pin = std::pair<int, BasePinPtr>;
using StatePins = std::array<Pin, Gpios>;
namespace settings {
namespace keys {
PROGMEM_STRING(Save, "fanSave");
PROGMEM_STRING(Speed, "fanSpeed");
// XXX: while these are hard-coded, we don't really benefit from having these in the hardware cfg
} // namespace keys
StatePins statePins() {
return {
{{5, nullptr},
{4, nullptr},
{15, nullptr}}
};
duration::Seconds save() {
return getSetting(keys::Save, build::SaveDelay);
}
constexpr int controlPin() {
return 12;
FanSpeed speed() {
return getSetting(keys::Speed, build::Speed);
}
struct Config {
Config() = default;
explicit Config(unsigned long save_, FanSpeed speed_) :
save(save_),
speed(speed_)
{}
unsigned long save { DefaultSaveDelay };
FanSpeed speed { FanSpeed::Off };
StatePins state_pins;
} // namespace settings
// We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed
// Sync up with the relay and write it on ON / OFF status events
struct Pin {
unsigned char init;
BasePinPtr handle;
};
Config readSettings() {
return Config(
getSetting("fanSave", DefaultSaveDelay),
getSetting("fanSpeed", FanSpeed::Medium)
);
}
struct StatePins {
static constexpr size_t Gpios { 3ul };
using State = std::array<int8_t, Gpios>;
using Pins = std::array<Pin, Gpios>;
Config config;
StatePins(const StatePins&) = delete;
void configure() {
config = readSettings();
}
StatePins() = default;
~StatePins() {
reset();
}
void report(FanSpeed speed [[gnu::unused]]) {
#if MQTT_SUPPORT
mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed));
#endif
}
StatePins(StatePins&&) = default;
void save(FanSpeed speed) {
static Ticker ticker;
config.speed = speed;
ticker.once_ms(config.save, []() {
const char* value = speedToPayload(config.speed);
setSetting("fanSpeed", value);
DEBUG_MSG_P(PSTR("[IFAN] Saved speed setting \"%s\"\n"), value);
});
}
void cleanupPins(StatePins& pins) {
for (auto& pin : pins) {
if (!pin.second) continue;
gpioUnlock(pin.second->pin());
pin.second.reset(nullptr);
bool init();
bool initialized() const {
return _initialized;
}
}
StatePins setupStatePins() {
StatePins pins = statePins();
void reset();
for (auto& pair : pins) {
auto ptr = gpioRegister(pair.first);
if (!ptr) {
DEBUG_MSG_P(PSTR("[IFAN] Could not set up GPIO%d\n"), pair.first);
cleanupPins(pins);
return pins;
}
ptr->pinMode(OUTPUT);
pair.second = std::move(ptr);
State state(FanSpeed);
State update(FanSpeed);
State state() const {
return _state;
}
return pins;
}
String toString() const;
private:
// XXX: while these are hard-coded, we don't really benefit from having these in the hardware cfg
bool _initialized { false };
Pins _pins{{
Pin{5, nullptr},
Pin{4, nullptr},
Pin{15, nullptr}}};
State _state{{LOW, LOW, LOW}};
};
State stateFromSpeed(FanSpeed speed) {
StatePins::State StatePins::state(FanSpeed speed) {
switch (speed) {
case FanSpeed::Low:
return {HIGH, LOW, LOW};
_state = {HIGH, LOW, LOW};
break;
case FanSpeed::Medium:
return {HIGH, HIGH, LOW};
_state = {HIGH, HIGH, LOW};
break;
case FanSpeed::High:
return {HIGH, LOW, HIGH};
_state = {HIGH, LOW, HIGH};
break;
case FanSpeed::Off:
_state = {LOW, LOW, LOW};
break;
}
return {LOW, LOW, LOW};
return _state;
}
const char* maskFromSpeed(FanSpeed speed) {
switch (speed) {
case FanSpeed::Low:
return "0b100";
case FanSpeed::Medium:
return "0b110";
case FanSpeed::High:
return "0b101";
case FanSpeed::Off:
return "0b000";
void StatePins::reset() {
for (auto& pin : _pins) {
if (pin.handle) {
gpioUnlock(pin.handle->pin());
pin.handle.reset(nullptr);
}
}
}
bool StatePins::init() {
if (_initialized) {
return true;
}
for (auto& pair : _pins) {
pair.handle = gpioRegister(pair.init);
if (!pair.handle) {
DEBUG_MSG_P(PSTR("[IFAN] Could not set up GPIO%hhu\n"), pair.init);
reset();
return false;
}
pair.handle->pinMode(OUTPUT);
}
return "";
_initialized = true;
return true;
}
// Note that we use API speed endpoint strictly for the setting
// (which also allows to pre-set the speed without turning the relay ON)
StatePins::State StatePins::update(FanSpeed speed) {
const auto out = state(speed);
for (size_t index = 0; index < _pins.size(); ++index) {
auto& handle = _pins[index].handle;
if (!handle) {
continue;
}
handle->digitalWrite(_state[index]);
}
using FanSpeedUpdate = std::function<void(FanSpeed)>;
return out;
}
#if DEBUG_SUPPORT
String StatePins::toString() const {
String out("0b000");
for (size_t index = 2; index != out.length(); ++index) {
out[index] = (_state[index - 2] == HIGH) ? '1' : '0';
}
return out;
}
#endif
struct ControlPin {
~ControlPin() {
reset();
}
explicit operator bool() const {
return static_cast<bool>(_pin);
}
ControlPin& operator=(uint8_t pin) {
reset();
_pin = gpioRegister(pin);
if (_pin) {
_pin->pinMode(OUTPUT);
}
return *this;
}
ControlPin& operator=(BasePinPtr pin) {
reset();
_pin = std::move(pin);
return *this;
}
FanSpeedUpdate onFanSpeedUpdate = [](FanSpeed) {
void reset() {
if (_pin) {
gpioUnlock(_pin->pin());
_pin.reset(nullptr);
}
}
BasePin* operator->() {
return _pin.get();
}
BasePin* operator->() const {
return _pin.get();
}
private:
BasePinPtr _pin;
};
void updateSpeed(Config& config, FanSpeed speed) {
switch (speed) {
case FanSpeed::Low:
case FanSpeed::Medium:
case FanSpeed::High:
save(speed);
report(speed);
onFanSpeedUpdate(speed);
break;
case FanSpeed::Off:
break;
struct Config {
duration::Seconds save;
FanSpeed speed;
};
namespace internal {
timer::SystemTimer config_timer;
Config config;
size_t relay_id { RelaysMax };
ControlPin control_pin;
FanSpeed speed { FanSpeed::Off };
StatePins state_pins;
} // namespace internal
bool currentStatus() {
return internal::speed != FanSpeed::Off;
}
void currentStatus(bool status) {
internal::speed = status
? internal::config.speed
: FanSpeed::Off;
}
FanSpeed currentSpeed() {
return internal::speed;
}
String speedToPayload() {
return speedToPayload(currentSpeed());
}
void save(FanSpeed speed) {
internal::config.speed = speed;
if (FanSpeed::Off != speed) {
internal::config_timer.once(
internal::config.save,
[speed]() {
const auto value = speedToPayload(speed);
setSetting(settings::keys::Speed, value);
DEBUG_MSG_P(PSTR("[IFAN] Saved speed \"%s\" (%s)\n"),
value.c_str(), internal::state_pins.toString().c_str());
});
}
}
void report(FanSpeed speed [[gnu::unused]]) {
#if MQTT_SUPPORT
mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed).c_str());
#endif
}
void pin_update(FanSpeed speed) {
const bool status = FanSpeed::Off != speed;
relayStatus(internal::relay_id, status);
internal::control_pin->digitalWrite(status ? HIGH : LOW);
internal::state_pins.update(speed);
}
void pin_update() {
pin_update(internal::speed);
}
FanSpeed update(FanSpeed value) {
const auto last = internal::speed;
if (value != last) {
save(value);
report(value);
}
internal::speed = value;
pin_update(value);
return value;
}
void updateSpeed(FanSpeed speed) {
updateSpeed(config, speed);
FanSpeed update(bool status) {
currentStatus(status);
return update(internal::speed);
}
void updateSpeedFromPayload(const char* payload) {
updateSpeed(payloadToSpeed(payload));
void configure() {
const auto updated = Config{
.save = settings::save(),
.speed = settings::speed()};
internal::config = updated;
pin_update();
}
void updateSpeedFromPayload(const String& payload) {
updateSpeedFromPayload(payload.c_str());
// Note that we use API speed endpoint strictly for the setting
// (which also allows to pre-set the speed without turning the relay ON)
FanSpeed updateSpeedFromPayload(StringView payload) {
return update(payloadToSpeed(payload.toString()));
}
#if MQTT_SUPPORT
void onMqttEvent(unsigned int type, const char* topic, const char* payload) {
void onMqttEvent(unsigned int type, StringView topic, StringView payload) {
switch (type) {
case MQTT_CONNECT_EVENT:
@ -267,67 +391,64 @@ void onMqttEvent(unsigned int type, const char* topic, const char* payload) {
#endif // MQTT_SUPPORT
class FanProvider : public RelayProviderBase {
class FanRelayProvider : public RelayProviderBase {
public:
explicit FanProvider(BasePinPtr&& pin, const Config& config, FanSpeedUpdate& callback) :
_pin(std::move(pin)),
_config(config)
{
callback = [this](FanSpeed speed) {
change(speed);
};
_pin->pinMode(OUTPUT);
espurna::StringView id() const override {
return STRING_VIEW("fan");
}
const char* id() const override {
return "fan";
void change(bool status) override {
ifan02::update(status);
}
void change(FanSpeed speed) {
_pin->digitalWrite((FanSpeed::Off != speed) ? HIGH : LOW);
private:
BasePinPtr _pin;
};
auto state = stateFromSpeed(speed);
DEBUG_MSG_P(PSTR("[IFAN] State mask: %s\n"), maskFromSpeed(speed));
#if TERMINAL_SUPPORT
namespace terminal {
for (size_t index = 0; index < _config.state_pins.size(); ++index) {
auto& pin = _config.state_pins[index].second;
if (!pin) {
continue;
}
PROGMEM_STRING(Speed, "SPEED");
pin->digitalWrite(state[index]);
}
void speed(::terminal::CommandContext&& ctx) {
auto value = ifan02::currentSpeed();
if (ctx.argv.size() == 2) {
value = updateSpeedFromPayload(ctx.argv[1]);
}
void change(bool status) override {
change(status ? _config.speed : FanSpeed::Off);
}
ctx.output.printf_P(PSTR("%s %s\n"),
(value != FanSpeed::Off)
? PSTR("speed")
: PSTR("fan is"),
speedToPayload(value).c_str());
terminalOK(ctx);
}
private:
BasePinPtr _pin;
const Config& _config;
static constexpr ::terminal::Command Commands[] PROGMEM {
{Speed, speed},
};
void setup() {
espurna::terminal::add(Commands);
}
config.state_pins = setupStatePins();
if (!config.state_pins.size()) {
return;
} // namespace terminal
#endif
bool setup() {
if (internal::control_pin && internal::state_pins.initialized()) {
return true;
}
configure();
internal::control_pin = build::ControlPin;
if (!internal::state_pins.init()) {
internal::control_pin.reset();
return false;
}
configure();
espurnaRegisterReload(configure);
auto relay_pin = gpioRegister(controlPin());
if (relay_pin) {
auto provider = std::make_unique<FanProvider>(std::move(relay_pin), config, onFanSpeedUpdate);
if (!relayAdd(std::move(provider))) {
DEBUG_MSG_P(PSTR("[IFAN] Could not add relay provider for GPIO%d\n"), controlPin());
gpioUnlock(controlPin());
}
}
#if MQTT_SUPPORT
mqttRegister(onMqttEvent);
#endif
@ -335,7 +456,7 @@ void setup() {
#if API_SUPPORT
apiRegister(F(MQTT_TOPIC_SPEED),
[](ApiRequest& request) {
request.send(speedToPayload(config.speed));
request.send(speedToPayload());
return true;
},
[](ApiRequest& request) {
@ -346,32 +467,50 @@ void setup() {
#endif
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("SPEED"), [](const terminal::CommandContext& ctx) {
if (ctx.argc == 2) {
updateSpeedFromPayload(ctx.argv[1]);
}
ctx.output.printf_P(PSTR("%s %s\n"),
(config.speed != FanSpeed::Off) ? "speed" : "fan is",
speedToPayload(config.speed));
terminalOK(ctx);
});
terminal::setup();
#endif
return true;
}
} // namespace ifan
RelayProviderBasePtr make_relay_provider(size_t index) {
RelayProviderBasePtr out;
if (setup()) {
out = std::make_unique<FanRelayProvider>();
internal::relay_id = index;
}
return out;
}
} // namespace
} // namespace ifan02
} // namespace espurna
RelayProviderBasePtr fanMakeRelayProvider(size_t index) {
return espurna::ifan02::make_relay_provider(index);
}
void fanStatus(bool value) {
espurna::ifan02::currentStatus(value);
}
bool fanStatus() {
return espurna::ifan02::currentStatus();
}
FanSpeed fanSpeed() {
return ifan02::config.speed;
return espurna::ifan02::currentSpeed();
}
void fanSpeed(FanSpeed speed) {
ifan02::updateSpeed(FanSpeed::Low);
espurna::ifan02::update(speed);
}
void fanSetup() {
ifan02::setup();
espurna::ifan02::setup();
}
#endif // IFAN_SUPPORT

+ 44
- 29
code/espurna/influxdb.cpp View File

@ -6,16 +6,17 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "influxdb.h"
#include "espurna.h"
#if INFLUXDB_SUPPORT
#include <map>
#include <memory>
#include "influxdb.h"
#include "mqtt.h"
#include "rpc.h"
#include "relay.h"
#include "rpc.h"
#include "sensor.h"
#include "terminal.h"
#include "ws.h"
@ -131,12 +132,12 @@ void _idbInitClient() {
// -----------------------------------------------------------------------------
bool _idbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "idb", 3) == 0);
bool _idbWebSocketOnKeyCheck(espurna::StringView key, const JsonVariant& value) {
return espurna::settings::query::samePrefix(key, STRING_VIEW("idb"));
}
void _idbWebSocketOnVisible(JsonObject& root) {
root["idbVisible"] = 1;
wsPayloadModule(root, PSTR("idb"));
}
void _idbWebSocketOnConnected(JsonObject& root) {
@ -157,8 +158,8 @@ void _idbConfigure() {
if (_idb_enabled && !_idb_client) _idbInitClient();
}
void _idbSendSensor(const String& topic, unsigned char id, double, const char* value) {
idbSend(topic.c_str(), id, value);
void _idbSendSensor(const espurna::sensor::Value& value) {
idbSend(magnitudeTypeTopic(value.type).c_str(), value.index, value.repr.c_str());
}
void _idbSendStatus(size_t id, bool status) {
@ -215,7 +216,7 @@ void _idbFlush() {
// TODO: should we always store specific pairs like tspk keeps relay / sensor readings?
// note that we also send heartbeat data, persistent values should be flagged
const String device = getSetting("hostname");
const String device = systemHostname();
_idb_client->payload = "";
for (auto& pair : _idb_client->values) {
@ -250,38 +251,59 @@ bool idbEnabled() {
return _idb_enabled;
}
bool _idbHeartbeat(heartbeat::Mask mask) {
if (mask & heartbeat::Report::Uptime)
idbSend(MQTT_TOPIC_UPTIME, String(systemUptime()).c_str());
bool _idbHeartbeat(espurna::heartbeat::Mask mask) {
if (mask & espurna::heartbeat::Report::Uptime)
idbSend(MQTT_TOPIC_UPTIME, String(systemUptime().count()).c_str());
if (mask & heartbeat::Report::Freeheap) {
if (mask & espurna::heartbeat::Report::Freeheap) {
auto stats = systemHeapStats();
idbSend(MQTT_TOPIC_FREEHEAP, String(stats.available).c_str());
}
if (mask & heartbeat::Report::Rssi)
if (mask & espurna::heartbeat::Report::Rssi)
idbSend(MQTT_TOPIC_RSSI, String(WiFi.RSSI()).c_str());
if ((mask & heartbeat::Report::Vcc) && (ADC_MODE_VALUE == ADC_VCC))
if ((mask & espurna::heartbeat::Report::Vcc) && (ADC_MODE_VALUE == ADC_VCC))
idbSend(MQTT_TOPIC_VCC, String(ESP.getVcc()).c_str());
if (mask & heartbeat::Report::Loadavg)
if (mask & espurna::heartbeat::Report::Loadavg)
idbSend(MQTT_TOPIC_LOADAVG, String(systemLoadAverage()).c_str());
if (mask & heartbeat::Report::Ssid)
if (mask & espurna::heartbeat::Report::Ssid)
idbSend(MQTT_TOPIC_SSID, WiFi.SSID().c_str());
if (mask & heartbeat::Report::Bssid)
if (mask & espurna::heartbeat::Report::Bssid)
idbSend(MQTT_TOPIC_BSSID, WiFi.BSSIDstr().c_str());
return true;
}
#if TERMINAL_SUPPORT
PROGMEM_STRING(IdbSend, "IDB.SEND");
static void idbTerminalSend(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 4) {
terminalError(ctx, F("idb.send <topic> <id> <value>"));
return;
}
idbSend(ctx.argv[1].c_str(), ctx.argv[2].toInt(), ctx.argv[3].c_str());
}
static constexpr ::terminal::Command IdbCommands[] {
{IdbSend, idbTerminalSend},
};
static void idbTerminalSetup() {
espurna::terminal::add(IdbCommands);
}
#endif
void idbSetup() {
systemHeartbeat(_idbHeartbeat);
systemHeartbeat(_idbHeartbeat,
getSetting("idbHbMode", heartbeat::currentMode()),
getSetting("idbHbIntvl", heartbeat::currentInterval()));
getSetting("idbHbMode", espurna::heartbeat::currentMode()),
getSetting("idbHbIntvl", espurna::heartbeat::currentInterval()));
_idbConfigure();
@ -293,25 +315,18 @@ void idbSetup() {
#endif
#if RELAY_SUPPORT
relaySetStatusChange(_idbSendStatus);
relayOnStatusChange(_idbSendStatus);
#endif
#if SENSOR_SUPPORT
sensorSetMagnitudeReport(_idbSendSensor);
sensorOnMagnitudeReport(_idbSendSensor);
#endif
espurnaRegisterReload(_idbConfigure);
espurnaRegisterLoop(_idbFlush);
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("IDB.SEND"), [](const terminal::CommandContext& ctx) {
if (ctx.argc != 4) {
terminalError(F("idb.send <topic> <id> <value>"));
return;
}
idbSend(ctx.argv[1].c_str(), ctx.argv[2].toInt(), ctx.argv[3].c_str());
});
idbTerminalSetup();
#endif
}


+ 0
- 2
code/espurna/influxdb.h View File

@ -6,8 +6,6 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "espurna.h"
bool idbSend(const char * topic, unsigned char id, const char * payload);
bool idbSend(const char * topic, const char * payload);
bool idbEnabled();


+ 2051
- 354
code/espurna/ir.cpp
File diff suppressed because it is too large
View File


+ 0
- 2
code/espurna/ir.h View File

@ -10,6 +10,4 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#pragma once
#include "espurna.h"
void irSetup();

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save